diff --git a/pom.xml b/pom.xml index 9a1889723d..40038b9eda 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..a41ddc5c48 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 595e5a4250..1d9f4998dc 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java index 60c3ca38cc..33b2e5c9e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java @@ -20,7 +20,6 @@ import java.util.HashSet; import java.util.Set; -import org.bson.UuidRepresentation; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -226,7 +225,6 @@ protected boolean autoIndexCreation() { protected MongoClientSettings mongoClientSettings() { MongoClientSettings.Builder builder = MongoClientSettings.builder(); - builder.uuidRepresentation(UuidRepresentation.STANDARD); configureClientSettings(builder); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java index 7c66396302..e9cb21e700 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java @@ -23,8 +23,8 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.bson.UuidRepresentation; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; @@ -162,7 +162,6 @@ protected MongoClientSettings computeClientSetting() { getOrDefault(port, "" + ServerAddress.defaultPort()))); Builder builder = MongoClientSettings.builder().applyConnectionString(connectionString); - builder.uuidRepresentation(UuidRepresentation.STANDARD); if (mongoClientSettings != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 24c3c2f590..fbd5d82cac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -1376,6 +1376,7 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist if (typeHint != null && Object.class != typeHint) { + // TODO this is weird and leads to double-conversion in some cases, e.g. BigDecimal -> Decimal128 -> BigDecimal if (conversionService.canConvert(value.getClass(), typeHint)) { value = doConvert(value, typeHint); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index 1fd45e1960..5cfd6fe72a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -23,9 +23,14 @@ import java.net.URI; import java.net.URL; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.Currency; +import java.util.Date; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -83,19 +88,65 @@ abstract class MongoConverters { private MongoConverters() {} /** - * Returns the converters to be registered. + * Returns the {@code Date} to UTC converters to be registered. * * @return - * @since 1.9 + * @since 5.0 */ - static Collection getConvertersToRegister() { + static Collection getDateToUtcConverters() { - List converters = new ArrayList<>(); + List converters = new ArrayList<>(3); + + converters.add(DateToUtcLocalDateConverter.INSTANCE); + converters.add(DateToUtcLocalTimeConverter.INSTANCE); + converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); + + return converters; + } + + /** + * Returns the {@code Decimal128} converters to be registered. + * + * @return + * @since 5.0 + */ + static Collection> getBigNumberDecimal128Converters() { + + List> converters = new ArrayList<>(3); converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); converters.add(BigIntegerToDecimal128Converter.INSTANCE); + return converters; + } + + /** + * Returns the {@code String} converters to be registered for {@link BigInteger} and {@link BigDecimal}. + * + * @return + * @since 5.0 + */ + static Collection> getBigNumberStringConverters() { + + List> converters = new ArrayList<>(2); + + converters.add(BigDecimalToStringConverter.INSTANCE); + converters.add(BigIntegerToStringConverter.INSTANCE); + + return converters; + } + + /** + * Returns the converters to be registered. + * + * @return + * @since 1.9 + */ + static Collection getConvertersToRegister() { + + List converters = new ArrayList<>(); + converters.add(URLToStringConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE); converters.add(DocumentToStringConverter.INSTANCE); @@ -116,6 +167,9 @@ static Collection getConvertersToRegister() { converters.add(ListToVectorConverter.INSTANCE); converters.add(BinaryVectorToMongoVectorConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converters.add(StringToBigIntegerConverter.INSTANCE); + converters.add(reading(BsonUndefined.class, Object.class, it -> null)); converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString)); @@ -174,6 +228,7 @@ public ObjectId convert(BigInteger source) { } } + @WritingConverter enum BigDecimalToStringConverter implements Converter { INSTANCE; @@ -185,6 +240,7 @@ public String convert(BigDecimal source) { /** * @since 2.2 */ + @WritingConverter enum BigDecimalToDecimal128Converter implements Converter { INSTANCE; @@ -196,6 +252,7 @@ public Decimal128 convert(BigDecimal source) { /** * @since 5.0 */ + @WritingConverter enum BigIntegerToDecimal128Converter implements Converter { INSTANCE; @@ -204,6 +261,7 @@ public Decimal128 convert(BigInteger source) { } } + @ReadingConverter enum StringToBigDecimalConverter implements Converter { INSTANCE; @@ -215,6 +273,7 @@ enum StringToBigDecimalConverter implements Converter { /** * @since 2.2 */ + @ReadingConverter enum Decimal128ToBigDecimalConverter implements Converter { INSTANCE; @@ -630,4 +689,35 @@ public Instant convert(BsonTimestamp source) { return Instant.ofEpochSecond(source.getTime(), 0); } } + + @ReadingConverter + private enum DateToUtcLocalDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalDateTime convert(Date source) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC")); + } + } + + @ReadingConverter + private enum DateToUtcLocalTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalTime convert(Date source) { + return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime(); + } + } + + @ReadingConverter + private enum DateToUtcLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(Date source) { + return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index f7af94728a..6915ae5b5f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core.convert; -import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -31,25 +30,25 @@ import java.util.Locale; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterRegistrar; -import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.SimplePropertyValueConversions; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.lang.Contract; @@ -68,7 +67,7 @@ */ public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions { - private static final StoreConversions STORE_CONVERSIONS; + private static final Log LOGGER = LogFactory.getLog(MongoCustomConversions.class); private static final List STORE_CONVERTERS; static { @@ -80,9 +79,14 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus converters.addAll(GeoConverters.getConvertersToRegister()); STORE_CONVERTERS = Collections.unmodifiableList(converters); - STORE_CONVERSIONS = StoreConversions.of(MongoSimpleTypes.HOLDER, STORE_CONVERTERS); } + /** + * Converters to be registered with the {@code ConversionService} but hidden from CustomConversions to avoid + * converter-based type hinting. + */ + private final List> fallbackConversionServiceConverters = new ArrayList<>(); + /** * Creates an empty {@link MongoCustomConversions} object. */ @@ -106,7 +110,12 @@ public MongoCustomConversions(List> converters) { * @since 2.3 */ protected MongoCustomConversions(MongoConverterConfigurationAdapter conversionConfiguration) { - super(conversionConfiguration.createConverterConfiguration()); + this(conversionConfiguration.createConverterConfiguration()); + } + + private MongoCustomConversions(MongoConverterConfiguration converterConfiguration) { + super(converterConfiguration); + this.fallbackConversionServiceConverters.addAll(converterConfiguration.fallbackConversionServiceConverters); } /** @@ -125,6 +134,12 @@ public static MongoCustomConversions create(Consumer> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class); + private static final Set> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, + LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; - private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128; + private @Nullable BigDecimalRepresentation bigDecimals; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); @@ -326,6 +342,7 @@ public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation re this.bigDecimals = representation; return this; } + /** * Optionally set the {@link PropertyValueConversions} to be applied during mapping. * @@ -368,79 +385,59 @@ PropertyValueConversions valueConversions() { return this.propertyValueConversions; } - ConverterConfiguration createConverterConfiguration() { + MongoConverterConfiguration createConverterConfiguration() { if (hasDefaultPropertyValueConversions() && propertyValueConversions instanceof SimplePropertyValueConversions svc) { svc.init(); } - List converters = new ArrayList<>(STORE_CONVERTERS.size() + 7); - - if (bigDecimals == BigDecimalRepresentation.STRING) { - - converters.add(BigDecimalToStringConverter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); - converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); + List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); + List> fallbackConversionServiceConverters = new ArrayList<>(5); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberStringConverters()); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + + if (bigDecimals == null) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info( + "No BigDecimal/BigInteger representation set. Choose 'BigDecimalRepresentation.DECIMAL128' or 'BigDecimalRepresentation.String' to store values in desired format."); + } + } else { + switch (bigDecimals) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + } } - if (!useNativeDriverJavaTimeCodecs) { + fallbackConversionServiceConverters.removeAll(storeConverters); - converters.addAll(customConverters); - return new ConverterConfiguration(STORE_CONVERSIONS, converters, convertiblePair -> true, - this.propertyValueConversions); - } - - /* - * We need to have those converters using UTC as the default ones would go on with the systemDefault. - */ - converters.add(DateToUtcLocalDateConverter.INSTANCE); - converters.add(DateToUtcLocalTimeConverter.INSTANCE); - converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); - converters.addAll(STORE_CONVERTERS); + if (useNativeDriverJavaTimeCodecs) { - StoreConversions storeConversions = StoreConversions - .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), converters); + /* + * We need to have those converters using UTC as the default ones would go on with the systemDefault. + */ + storeConverters.addAll(MongoConverters.getDateToUtcConverters()); + storeConverters.addAll(STORE_CONVERTERS); - return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> { + StoreConversions storeConversions = StoreConversions + .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), storeConverters); - // Avoid default registrations + return new MongoConverterConfiguration(storeConversions, fallbackConversionServiceConverters, + this.customConverters, convertiblePair -> { - return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) - || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); - }, this.propertyValueConversions); - } + // Avoid default registrations - @ReadingConverter - private enum DateToUtcLocalDateTimeConverter implements Converter { + return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) + || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); + }, this.propertyValueConversions); - INSTANCE; - - @Override - public LocalDateTime convert(Date source) { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC")); } - } - @ReadingConverter - private enum DateToUtcLocalTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalTime convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime(); - } - } - - @ReadingConverter - private enum DateToUtcLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate(); - } + storeConverters.addAll(STORE_CONVERTERS); + return new MongoConverterConfiguration( + StoreConversions.of(MongoSimpleTypes.createSimpleTypeHolder(), storeConverters), + fallbackConversionServiceConverters, this.customConverters, convertiblePair -> true, + this.propertyValueConversions); } private boolean hasDefaultPropertyValueConversions() { @@ -449,6 +446,19 @@ private boolean hasDefaultPropertyValueConversions() { } + static class MongoConverterConfiguration extends ConverterConfiguration { + + private final List> fallbackConversionServiceConverters; + + public MongoConverterConfiguration(StoreConversions storeConversions, + List> fallbackConversionServiceConverters, List> userConverters, + Predicate converterRegistrationFilter, + @Nullable PropertyValueConversions propertyValueConversions) { + super(storeConversions, userConverters, converterRegistrationFilter, propertyValueConversions); + this.fallbackConversionServiceConverters = fallbackConversionServiceConverters; + } + } + /** * Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 6b4d9b9e9b..2c1b6af660 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.mapping; +import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; import java.util.Set; @@ -60,24 +61,29 @@ public abstract class MongoSimpleTypes { BsonDocument.class, BsonDouble.class, BsonInt32.class, BsonInt64.class, BsonJavaScript.class, BsonJavaScriptWithScope.class, BsonObjectId.class, BsonRegularExpression.class, BsonString.class, BsonTimestamp.class, Geometry.class, GeometryCollection.class, LineString.class, MultiLineString.class, - MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class); + MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class, BigInteger.class, BigDecimal.class); - public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { + public static final SimpleTypeHolder HOLDER = createSimpleTypeHolder(); - @Override - public boolean isSimpleType(Class> type) { + public static SimpleTypeHolder createSimpleTypeHolder() { - if (type.isEnum()) { - return true; - } + return new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { - if (type.getName().startsWith("java.time")) { - return false; - } + @Override + public boolean isSimpleType(Class> type) { + + if (type.isEnum()) { + return true; + } - return super.isSimpleType(type); - } - }; + if (type.getName().startsWith("java.time")) { + return false; + } + + return super.isSimpleType(type); + } + }; + } private MongoSimpleTypes() {} } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java index bbb3c0eaef..5707772fcf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation.Alternate; import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 362044f35e..9146321e06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -15,10 +15,14 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; -import static org.springframework.data.mongodb.core.query.Update.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.data.mongodb.core.query.Criteria.expr; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; @@ -28,17 +32,29 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.convert.ConversionFailedException; @@ -69,6 +85,7 @@ import org.springframework.data.mongodb.core.convert.LazyLoadingProxy; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; @@ -144,6 +161,12 @@ public class MongoTemplateTests { it.defaultDb(DB_NAME); }); + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + adapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); + }); + cfg.configureMappingContext(it -> { it.autocreateIndex(false); it.initialEntitySet(AuditablePerson.class); @@ -170,7 +193,10 @@ public class MongoTemplateTests { }); cfg.configureConversion(it -> { - it.customConverters(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE); + it.customConverters(adapter -> { + adapter.registerConverters(List.of(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE)) + .bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); }); cfg.configureMappingContext(it -> { @@ -732,7 +758,7 @@ public void testDistinct() { .containsExactlyInAnyOrder(person1.getName(), person2.getName()); assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", template.getCollectionName(MyPerson.class), MyPerson.class, String.class)) - .containsExactlyInAnyOrder(person1.getName(), person2.getName()); + .containsExactlyInAnyOrder(person1.getName(), person2.getName()); } @Test // DATAMONGO-1761 @@ -876,7 +902,7 @@ public void testUsingAnInQueryWithLongId() throws Exception { } @Test // DATAMONGO-602, GH-4920 - public void testUsingAnInQueryWithBigIntegerId() throws Exception { + public void testUsingAnInQueryWithBigIntegerId() { template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); @@ -887,6 +913,34 @@ public void testUsingAnInQueryWithBigIntegerId() throws Exception { assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1)); } + @Test // GH-5037 + public void errorsIfNoBigNumberFormatDefined() { + + template = new MongoTestTemplate(cfg -> { + + cfg.configureDatabaseFactory(it -> { + + it.client(client); + it.defaultDb(DB_NAME); + }); + + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + // no numeric conversion + }); + }); + + }); + + template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); + + PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger(); + p1.setFirstName("Sven"); + p1.setAge(11); + p1.setId(new BigInteger("2666666666666666665")); + assertThatExceptionOfType(CodecConfigurationException.class).isThrownBy(() -> template.insert(p1)); + } + @Test public void testUsingAnInQueryWithPrimitiveIntId() throws Exception { @@ -2561,9 +2615,7 @@ public void shouldReadNestedProjection() { walter.address = new Address("spring", "data"); template.save(walter); - PersonPWA result = template.query(MyPerson.class) - .as(PersonPWA.class) - .matching(where("id").is(walter.id)) + PersonPWA result = template.query(MyPerson.class).as(PersonPWA.class).matching(where("id").is(walter.id)) .firstValue(); assertThat(result.getAddress().getCity()).isEqualTo("data"); @@ -2571,6 +2623,7 @@ public void shouldReadNestedProjection() { interface PersonPWA { String getName(); + AdressProjection getAddress(); } @@ -2823,7 +2876,7 @@ public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() { assertThat(template.getDb().getCollection("sample").countDocuments( new org.bson.Document("field", new org.bson.Document("$in", Arrays.asList("spring", "mongodb"))))) - .isEqualTo(0L); + .isEqualTo(0L); assertThat(template.getDb().getCollection("sample").countDocuments(new org.bson.Document("field", "data"))) .isEqualTo(1L); } @@ -3935,7 +3988,8 @@ void saveEntityWithDotInFieldName() { template.save(source); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "v1"); } @@ -3954,13 +4008,17 @@ void queryEntityWithDotInFieldNameUsingExpr() { template.save(source); template.save(source2); - WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname mapping - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))).firstValue(); + WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname + // mapping + .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))).firstValue(); + .matching( + expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); } @@ -3975,20 +4033,20 @@ void updateEntityWithDotInFieldNameUsingAggregations() { template.save(source); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))) - .first(); + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)).apply(AggregationUpdate + .newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))).first(); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed"); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)) + .apply(AggregationUpdate.newUpdate( + ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) .first(); - raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed-again"); } @@ -4013,9 +4071,8 @@ void savesMapWithDotInKey() { org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); - assertThat(raw.get("mapValue", org.bson.Document.class)) - .containsEntry("k1", "v1") - .containsEntry("map.key.with.dot", "v2"); + assertThat(raw.get("mapValue", org.bson.Document.class)).containsEntry("k1", "v1").containsEntry("map.key.with.dot", + "v2"); } @Test // GH-4464 @@ -4031,16 +4088,13 @@ void readsMapWithDotInKey() { MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter); Map sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2"); - template.execute(WithFieldNameContainingDots.class, - collection -> { - collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); - return null; - } - ); + template.execute(WithFieldNameContainingDots.class, collection -> { + collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); + return null; + }); WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) - .matching(where("id").is("id-1")) - .firstValue(); + .matching(where("id").is("id-1")).firstValue(); assertThat(loaded.mapValue).isEqualTo(sourceMap); } @@ -5015,8 +5069,7 @@ static class WithFieldNameContainingDots { String id; - @Field(value = "field.name.with.dots", nameType = Type.KEY) - String value; + @Field(value = "field.name.with.dots", nameType = Type.KEY) String value; Map mapValue; @@ -5034,7 +5087,8 @@ public boolean equals(Object o) { return false; } WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o; - return Objects.equals(id, withFieldNameContainingDots.id) && Objects.equals(value, withFieldNameContainingDots.value) + return Objects.equals(id, withFieldNameContainingDots.id) + && Objects.equals(value, withFieldNameContainingDots.value) && Objects.equals(mapValue, withFieldNameContainingDots.mapValue); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 1f0af2851e..6523b196bf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -90,6 +90,8 @@ import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -135,8 +137,8 @@ class MappingMongoConverterUnitTests { @BeforeEach void beforeEach() { - MongoCustomConversions conversions = new MongoCustomConversions( - Arrays.asList(new ByteBufferToDoubleHolderConverter())); + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter() + .registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(BigDecimalRepresentation.DECIMAL128)); mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -398,6 +400,22 @@ void writesClassWithBigDecimal() { assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); } + @Test // DATACMNS-42, DATAMONGO-171, GH-4920 + void writesClassWithBigDecimalFails() { + + converter = createConverter(); + + BigDecimalContainer container = new BigDecimalContainer(); + container.value = BigDecimal.valueOf(2.5d); + container.map = Collections.singletonMap("foo", container.value); + + org.bson.Document document = new org.bson.Document(); + converter.write(container, document); + + assertThat(document.get("value")).isInstanceOf(BigDecimal.class); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(BigDecimal.class); + } + @Test // DATACMNS-42, DATAMONGO-171 void readsClassWithBigDecimal() { @@ -2149,6 +2167,159 @@ void mapsBigDecimalToDecimal128WhenAnnotatedWithFieldTargetType() { assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigNumbersToString() { + + converter = createConverter(BigDecimalRepresentation.STRING); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger.toString()); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal.toString()); + } + + @Test // GH-5037 + void mapsBigNumbersToDecimal128() { + + converter = createConverter(BigDecimalRepresentation.DECIMAL128); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitBigIntegerToStringConversion(BigDecimalRepresentation representation) { + + converter = createConverter(representation); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigIntegerAsString = BigInteger.TWO; + source.bigDecimalAsString = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString()); + assertThat(target.get("bigDecimalAsString")).isEqualTo(source.bigDecimalAsString.toString()); + } + + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitDecimal128Conversion(BigDecimalRepresentation representation) { + + converter = createConverter(representation); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @SuppressWarnings("deprecation") + static Stream representations() { + + return Stream.of(Arguments.argumentSet("None (default)", new Object[] { null }), // + Arguments.argumentSet("STRING", BigDecimalRepresentation.STRING), // + Arguments.argumentSet("DECIMAL128", BigDecimalRepresentation.DECIMAL128)); + } + + @Test // GH-5037 + void shouldWriteBigNumbersAsIsWithoutConfiguration() { + + converter = createConverter(); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromDecimal128() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); + + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigIntegerAsString", "2"); + target.put("bigDecimalAsString", "123.456"); + + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigIntegerAsString).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimalAsString).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromDecimal128() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", "2"); + target.put("bigDecimal", "123.456"); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + @Test // DATAMONGO-2328 void mapsDateToLongWhenAnnotatedWithFieldTargetType() { @@ -3171,7 +3342,6 @@ void beanConverter() { return nativeValue.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); @@ -3407,11 +3577,21 @@ void usesStringNumericFormat() { assertThat(document).containsEntry("map.foo", "2.5"); } + private MappingMongoConverter createConverter() { + return createConverter(null); + } + private MappingMongoConverter createConverter( MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { MongoCustomConversions conversions = MongoCustomConversions.create( - it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); + it -> { + it.registerConverter(new ByteBufferToDoubleHolderConverter()); + + if (bigDecimalRepresentation != null) { + it.bigDecimal(bigDecimalRepresentation); + } + }); MongoMappingContext mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -4081,7 +4261,14 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; - @Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; + @Field(targetType = FieldType.DECIMAL128) // + BigInteger bigInteger; + + @Field(targetType = FieldType.STRING) // + BigInteger bigIntegerAsString; + + @Field(targetType = FieldType.STRING) // + BigDecimal bigDecimalAsString; @Field(targetType = FieldType.INT64) // Date dateAsLong; @@ -4096,6 +4283,14 @@ static class WithExplicitTargetTypes { Date dateAsObjectId; } + static class WithoutExplicitTargetTypes { + + BigDecimal bigDecimal; + + BigInteger bigInteger; + + } + static class WrapperAroundWithUnwrapped { String someValue; @@ -4211,7 +4406,6 @@ public SubTypeOfGenericType convert(org.bson.Document source) { @WritingConverter static class TypeImplementingMapToDocumentConverter implements Converter { - @Override public org.bson.@Nullable Document convert(TypeImplementingMap source) { return new org.bson.Document("1st", source.val1).append("2nd", source.val2); @@ -4413,7 +4607,6 @@ enum Converter2 implements MongoValueConverter { return value.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("bar", value); @@ -4427,7 +4620,6 @@ static class Converter1 implements MongoValueConverter... converters) { customConversions(new MongoCustomConversions(Arrays.asList(converters))); } + + public void customConverters(Consumer configurer) { + customConversions(MongoCustomConversions.create(configurer)); + } } diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml index 79e5ac40a0..fcf8a258da 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml @@ -3,10 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:context="http://www.springframework.org/schema/context" - xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> - + @@ -26,8 +25,8 @@ - - @@ -56,9 +55,9 @@ - + - + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml index 8c6194f0a4..a0451bb97e 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml @@ -1,12 +1,20 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml index 81a7c261f9..c802ee7c36 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml @@ -7,8 +7,6 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - - diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml index b70efb607c..2993ee1e46 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml @@ -9,7 +9,11 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - + + + + + diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 6f2d1e2847..ac69429f33 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -3,6 +3,7 @@ ** xref:migration-guides.adoc[] *** xref:migration-guide/migration-guide-2.x-to-3.x.adoc[] *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] +*** xref:migration-guide/migration-guide-4.x-to-5.x.adoc[] * xref:mongodb.adoc[] ** xref:preface.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc new file mode 100644 index 0000000000..4351cc70e7 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -0,0 +1,59 @@ +[[mongodb.migration.4.x-5.x]] += Migration Guide from 4.x to 5.x + +Spring Data MongoDB 4.x requires the MongoDB Java Driver 5.5.x + +To learn more about driver versions please visit the https://www.mongodb.com/docs/drivers/java/sync/current/upgrade/[MongoDB Documentation]. + +== UUID Representation Changes + +Spring Data no longer defaults UUID settings via its configuration support classes, factory beans, nor XML namespace. + +In order to persist UUID values the `UuidRepresentation` hast to be set explicitly. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { + builder.uuidRepresentation(UuidRepresentation.STANDARD); + } + + // ... +} +---- + +XML:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +---- + + + +---- +====== + +== BigInteger/BigDecimal Conversion Changes + +Spring Data no longer defaults BigInteger/BigDecimal conversion via its configuration support classes. +In order to persist those values the default `BigDecimalRepresentation` hast to be set explicitly. + +[source,java] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter configAdapter) { + configAdapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + } + + // ... +} +---- + +Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default to retain previous behaviour. diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc index 5a038dde66..7ddc2bf4ec 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc @@ -4,7 +4,7 @@ include::{commons}@data-commons::page$custom-conversions.adoc[] == Type based Converter The most trivial way of influencing the mapping result is by specifying the desired native MongoDB target type via the `@Field` annotation. -This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in native `org.bson.types.Decimal128` format. +This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in eg. `String` format. .Explicit target type mapping ==== @@ -33,8 +33,7 @@ public class Payment { <1> String _id_ values that represent a valid `ObjectId` are converted automatically. See xref:mongodb/template-crud-operations.adoc#mongo-template.id-handling[How the `_id` Field is Handled in the Mapping Layer] for details. <2> The desired target type is explicitly defined as `String`. -Otherwise, the -`BigDecimal` value would have been turned into a `Decimal128`. +Otherwise. <3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. ==== @@ -113,8 +112,10 @@ To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted v This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc. With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`. -As of Spring Data MongoDB 5.0. the default representation of those types moved to MongoDB native `org.bson.types.Decimal128`. -You can still use the to the previous `String` variant by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING))`. +As of Spring Data MongoDB 5.0. there no longer is a default representation of those types and conversion needs to be configured explicitly. +You can register multiple formats, 1st being default, and still retain the previous behaviour by configuring the `BigDecimalRepresentation` in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128))`. +This allows you to make use of the explicit storage type format via `@Field(targetType = DECIMAL128)` while keeping default conversion set to String. +Choosing none of the provided representations is valid as long as those values are no persisted. [NOTE] ==== diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index 121c80d563..2056dfa20e 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -119,7 +119,7 @@ See xref:mongodb/mapping/custom-conversions.adoc[Custom Conversions - Overriding | native | `{"bin" : { "$binary" : "AQIDBA==", "$type" : "00" }}` -| `java.util.UUID` (Standard UUID) +| `java.util.UUID` (According to UuidRepresentation) | native | `{"uuid" : { "$binary" : "MEaf1CFQ6lSphaa3b9AtlA==", "$type" : "04" }}` @@ -164,13 +164,13 @@ calling `get()` before the actual conversion | `{"value" : "741" }` | `BigInteger` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741) }`, `{"value" : "741" }` | `BigDecimal` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }` | `URL` @@ -200,25 +200,21 @@ calling `get()` before the actual conversion | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `Instant` + -(Joda, JSR310-BackPort) +(Java 8) | converter | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `LocalDate` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T00:00:00.000Z")}` | `LocalDateTime`, `LocalTime` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] -| `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` - -| `DateTime` (Joda) -| converter +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` -| `ZoneId` (Java 8, JSR310-BackPort) +| `ZoneId` (Java 8) | converter | `{"zoneId" : "ECT - Europe/Paris"}`
@@ -368,79 +385,59 @@ PropertyValueConversions valueConversions() { return this.propertyValueConversions; } - ConverterConfiguration createConverterConfiguration() { + MongoConverterConfiguration createConverterConfiguration() { if (hasDefaultPropertyValueConversions() && propertyValueConversions instanceof SimplePropertyValueConversions svc) { svc.init(); } - List converters = new ArrayList<>(STORE_CONVERTERS.size() + 7); - - if (bigDecimals == BigDecimalRepresentation.STRING) { - - converters.add(BigDecimalToStringConverter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); - converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); + List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); + List> fallbackConversionServiceConverters = new ArrayList<>(5); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberStringConverters()); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + + if (bigDecimals == null) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info( + "No BigDecimal/BigInteger representation set. Choose 'BigDecimalRepresentation.DECIMAL128' or 'BigDecimalRepresentation.String' to store values in desired format."); + } + } else { + switch (bigDecimals) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + } } - if (!useNativeDriverJavaTimeCodecs) { + fallbackConversionServiceConverters.removeAll(storeConverters); - converters.addAll(customConverters); - return new ConverterConfiguration(STORE_CONVERSIONS, converters, convertiblePair -> true, - this.propertyValueConversions); - } - - /* - * We need to have those converters using UTC as the default ones would go on with the systemDefault. - */ - converters.add(DateToUtcLocalDateConverter.INSTANCE); - converters.add(DateToUtcLocalTimeConverter.INSTANCE); - converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); - converters.addAll(STORE_CONVERTERS); + if (useNativeDriverJavaTimeCodecs) { - StoreConversions storeConversions = StoreConversions - .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), converters); + /* + * We need to have those converters using UTC as the default ones would go on with the systemDefault. + */ + storeConverters.addAll(MongoConverters.getDateToUtcConverters()); + storeConverters.addAll(STORE_CONVERTERS); - return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> { + StoreConversions storeConversions = StoreConversions + .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), storeConverters); - // Avoid default registrations + return new MongoConverterConfiguration(storeConversions, fallbackConversionServiceConverters, + this.customConverters, convertiblePair -> { - return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) - || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); - }, this.propertyValueConversions); - } + // Avoid default registrations - @ReadingConverter - private enum DateToUtcLocalDateTimeConverter implements Converter { + return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) + || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); + }, this.propertyValueConversions); - INSTANCE; - - @Override - public LocalDateTime convert(Date source) { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC")); } - } - @ReadingConverter - private enum DateToUtcLocalTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalTime convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime(); - } - } - - @ReadingConverter - private enum DateToUtcLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate(); - } + storeConverters.addAll(STORE_CONVERTERS); + return new MongoConverterConfiguration( + StoreConversions.of(MongoSimpleTypes.createSimpleTypeHolder(), storeConverters), + fallbackConversionServiceConverters, this.customConverters, convertiblePair -> true, + this.propertyValueConversions); } private boolean hasDefaultPropertyValueConversions() { @@ -449,6 +446,19 @@ private boolean hasDefaultPropertyValueConversions() { } + static class MongoConverterConfiguration extends ConverterConfiguration { + + private final List> fallbackConversionServiceConverters; + + public MongoConverterConfiguration(StoreConversions storeConversions, + List> fallbackConversionServiceConverters, List> userConverters, + Predicate converterRegistrationFilter, + @Nullable PropertyValueConversions propertyValueConversions) { + super(storeConversions, userConverters, converterRegistrationFilter, propertyValueConversions); + this.fallbackConversionServiceConverters = fallbackConversionServiceConverters; + } + } + /** * Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 6b4d9b9e9b..2c1b6af660 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.mapping; +import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; import java.util.Set; @@ -60,24 +61,29 @@ public abstract class MongoSimpleTypes { BsonDocument.class, BsonDouble.class, BsonInt32.class, BsonInt64.class, BsonJavaScript.class, BsonJavaScriptWithScope.class, BsonObjectId.class, BsonRegularExpression.class, BsonString.class, BsonTimestamp.class, Geometry.class, GeometryCollection.class, LineString.class, MultiLineString.class, - MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class); + MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class, BigInteger.class, BigDecimal.class); - public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { + public static final SimpleTypeHolder HOLDER = createSimpleTypeHolder(); - @Override - public boolean isSimpleType(Class> type) { + public static SimpleTypeHolder createSimpleTypeHolder() { - if (type.isEnum()) { - return true; - } + return new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { - if (type.getName().startsWith("java.time")) { - return false; - } + @Override + public boolean isSimpleType(Class> type) { + + if (type.isEnum()) { + return true; + } - return super.isSimpleType(type); - } - }; + if (type.getName().startsWith("java.time")) { + return false; + } + + return super.isSimpleType(type); + } + }; + } private MongoSimpleTypes() {} } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java index bbb3c0eaef..5707772fcf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation.Alternate; import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 362044f35e..9146321e06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -15,10 +15,14 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; -import static org.springframework.data.mongodb.core.query.Update.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.data.mongodb.core.query.Criteria.expr; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; @@ -28,17 +32,29 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.convert.ConversionFailedException; @@ -69,6 +85,7 @@ import org.springframework.data.mongodb.core.convert.LazyLoadingProxy; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; @@ -144,6 +161,12 @@ public class MongoTemplateTests { it.defaultDb(DB_NAME); }); + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + adapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); + }); + cfg.configureMappingContext(it -> { it.autocreateIndex(false); it.initialEntitySet(AuditablePerson.class); @@ -170,7 +193,10 @@ public class MongoTemplateTests { }); cfg.configureConversion(it -> { - it.customConverters(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE); + it.customConverters(adapter -> { + adapter.registerConverters(List.of(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE)) + .bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); }); cfg.configureMappingContext(it -> { @@ -732,7 +758,7 @@ public void testDistinct() { .containsExactlyInAnyOrder(person1.getName(), person2.getName()); assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", template.getCollectionName(MyPerson.class), MyPerson.class, String.class)) - .containsExactlyInAnyOrder(person1.getName(), person2.getName()); + .containsExactlyInAnyOrder(person1.getName(), person2.getName()); } @Test // DATAMONGO-1761 @@ -876,7 +902,7 @@ public void testUsingAnInQueryWithLongId() throws Exception { } @Test // DATAMONGO-602, GH-4920 - public void testUsingAnInQueryWithBigIntegerId() throws Exception { + public void testUsingAnInQueryWithBigIntegerId() { template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); @@ -887,6 +913,34 @@ public void testUsingAnInQueryWithBigIntegerId() throws Exception { assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1)); } + @Test // GH-5037 + public void errorsIfNoBigNumberFormatDefined() { + + template = new MongoTestTemplate(cfg -> { + + cfg.configureDatabaseFactory(it -> { + + it.client(client); + it.defaultDb(DB_NAME); + }); + + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + // no numeric conversion + }); + }); + + }); + + template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); + + PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger(); + p1.setFirstName("Sven"); + p1.setAge(11); + p1.setId(new BigInteger("2666666666666666665")); + assertThatExceptionOfType(CodecConfigurationException.class).isThrownBy(() -> template.insert(p1)); + } + @Test public void testUsingAnInQueryWithPrimitiveIntId() throws Exception { @@ -2561,9 +2615,7 @@ public void shouldReadNestedProjection() { walter.address = new Address("spring", "data"); template.save(walter); - PersonPWA result = template.query(MyPerson.class) - .as(PersonPWA.class) - .matching(where("id").is(walter.id)) + PersonPWA result = template.query(MyPerson.class).as(PersonPWA.class).matching(where("id").is(walter.id)) .firstValue(); assertThat(result.getAddress().getCity()).isEqualTo("data"); @@ -2571,6 +2623,7 @@ public void shouldReadNestedProjection() { interface PersonPWA { String getName(); + AdressProjection getAddress(); } @@ -2823,7 +2876,7 @@ public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() { assertThat(template.getDb().getCollection("sample").countDocuments( new org.bson.Document("field", new org.bson.Document("$in", Arrays.asList("spring", "mongodb"))))) - .isEqualTo(0L); + .isEqualTo(0L); assertThat(template.getDb().getCollection("sample").countDocuments(new org.bson.Document("field", "data"))) .isEqualTo(1L); } @@ -3935,7 +3988,8 @@ void saveEntityWithDotInFieldName() { template.save(source); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "v1"); } @@ -3954,13 +4008,17 @@ void queryEntityWithDotInFieldNameUsingExpr() { template.save(source); template.save(source2); - WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname mapping - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))).firstValue(); + WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname + // mapping + .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))).firstValue(); + .matching( + expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); } @@ -3975,20 +4033,20 @@ void updateEntityWithDotInFieldNameUsingAggregations() { template.save(source); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))) - .first(); + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)).apply(AggregationUpdate + .newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))).first(); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed"); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)) + .apply(AggregationUpdate.newUpdate( + ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) .first(); - raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed-again"); } @@ -4013,9 +4071,8 @@ void savesMapWithDotInKey() { org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); - assertThat(raw.get("mapValue", org.bson.Document.class)) - .containsEntry("k1", "v1") - .containsEntry("map.key.with.dot", "v2"); + assertThat(raw.get("mapValue", org.bson.Document.class)).containsEntry("k1", "v1").containsEntry("map.key.with.dot", + "v2"); } @Test // GH-4464 @@ -4031,16 +4088,13 @@ void readsMapWithDotInKey() { MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter); Map sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2"); - template.execute(WithFieldNameContainingDots.class, - collection -> { - collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); - return null; - } - ); + template.execute(WithFieldNameContainingDots.class, collection -> { + collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); + return null; + }); WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) - .matching(where("id").is("id-1")) - .firstValue(); + .matching(where("id").is("id-1")).firstValue(); assertThat(loaded.mapValue).isEqualTo(sourceMap); } @@ -5015,8 +5069,7 @@ static class WithFieldNameContainingDots { String id; - @Field(value = "field.name.with.dots", nameType = Type.KEY) - String value; + @Field(value = "field.name.with.dots", nameType = Type.KEY) String value; Map mapValue; @@ -5034,7 +5087,8 @@ public boolean equals(Object o) { return false; } WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o; - return Objects.equals(id, withFieldNameContainingDots.id) && Objects.equals(value, withFieldNameContainingDots.value) + return Objects.equals(id, withFieldNameContainingDots.id) + && Objects.equals(value, withFieldNameContainingDots.value) && Objects.equals(mapValue, withFieldNameContainingDots.mapValue); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 1f0af2851e..6523b196bf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -90,6 +90,8 @@ import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -135,8 +137,8 @@ class MappingMongoConverterUnitTests { @BeforeEach void beforeEach() { - MongoCustomConversions conversions = new MongoCustomConversions( - Arrays.asList(new ByteBufferToDoubleHolderConverter())); + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter() + .registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(BigDecimalRepresentation.DECIMAL128)); mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -398,6 +400,22 @@ void writesClassWithBigDecimal() { assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); } + @Test // DATACMNS-42, DATAMONGO-171, GH-4920 + void writesClassWithBigDecimalFails() { + + converter = createConverter(); + + BigDecimalContainer container = new BigDecimalContainer(); + container.value = BigDecimal.valueOf(2.5d); + container.map = Collections.singletonMap("foo", container.value); + + org.bson.Document document = new org.bson.Document(); + converter.write(container, document); + + assertThat(document.get("value")).isInstanceOf(BigDecimal.class); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(BigDecimal.class); + } + @Test // DATACMNS-42, DATAMONGO-171 void readsClassWithBigDecimal() { @@ -2149,6 +2167,159 @@ void mapsBigDecimalToDecimal128WhenAnnotatedWithFieldTargetType() { assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigNumbersToString() { + + converter = createConverter(BigDecimalRepresentation.STRING); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger.toString()); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal.toString()); + } + + @Test // GH-5037 + void mapsBigNumbersToDecimal128() { + + converter = createConverter(BigDecimalRepresentation.DECIMAL128); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitBigIntegerToStringConversion(BigDecimalRepresentation representation) { + + converter = createConverter(representation); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigIntegerAsString = BigInteger.TWO; + source.bigDecimalAsString = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString()); + assertThat(target.get("bigDecimalAsString")).isEqualTo(source.bigDecimalAsString.toString()); + } + + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitDecimal128Conversion(BigDecimalRepresentation representation) { + + converter = createConverter(representation); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @SuppressWarnings("deprecation") + static Stream representations() { + + return Stream.of(Arguments.argumentSet("None (default)", new Object[] { null }), // + Arguments.argumentSet("STRING", BigDecimalRepresentation.STRING), // + Arguments.argumentSet("DECIMAL128", BigDecimalRepresentation.DECIMAL128)); + } + + @Test // GH-5037 + void shouldWriteBigNumbersAsIsWithoutConfiguration() { + + converter = createConverter(); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromDecimal128() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); + + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigIntegerAsString", "2"); + target.put("bigDecimalAsString", "123.456"); + + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigIntegerAsString).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimalAsString).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromDecimal128() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", "2"); + target.put("bigDecimal", "123.456"); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + @Test // DATAMONGO-2328 void mapsDateToLongWhenAnnotatedWithFieldTargetType() { @@ -3171,7 +3342,6 @@ void beanConverter() { return nativeValue.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); @@ -3407,11 +3577,21 @@ void usesStringNumericFormat() { assertThat(document).containsEntry("map.foo", "2.5"); } + private MappingMongoConverter createConverter() { + return createConverter(null); + } + private MappingMongoConverter createConverter( MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { MongoCustomConversions conversions = MongoCustomConversions.create( - it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); + it -> { + it.registerConverter(new ByteBufferToDoubleHolderConverter()); + + if (bigDecimalRepresentation != null) { + it.bigDecimal(bigDecimalRepresentation); + } + }); MongoMappingContext mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -4081,7 +4261,14 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; - @Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; + @Field(targetType = FieldType.DECIMAL128) // + BigInteger bigInteger; + + @Field(targetType = FieldType.STRING) // + BigInteger bigIntegerAsString; + + @Field(targetType = FieldType.STRING) // + BigDecimal bigDecimalAsString; @Field(targetType = FieldType.INT64) // Date dateAsLong; @@ -4096,6 +4283,14 @@ static class WithExplicitTargetTypes { Date dateAsObjectId; } + static class WithoutExplicitTargetTypes { + + BigDecimal bigDecimal; + + BigInteger bigInteger; + + } + static class WrapperAroundWithUnwrapped { String someValue; @@ -4211,7 +4406,6 @@ public SubTypeOfGenericType convert(org.bson.Document source) { @WritingConverter static class TypeImplementingMapToDocumentConverter implements Converter { - @Override public org.bson.@Nullable Document convert(TypeImplementingMap source) { return new org.bson.Document("1st", source.val1).append("2nd", source.val2); @@ -4413,7 +4607,6 @@ enum Converter2 implements MongoValueConverter { return value.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("bar", value); @@ -4427,7 +4620,6 @@ static class Converter1 implements MongoValueConverter... converters) { customConversions(new MongoCustomConversions(Arrays.asList(converters))); } + + public void customConverters(Consumer configurer) { + customConversions(MongoCustomConversions.create(configurer)); + } } diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml index 79e5ac40a0..fcf8a258da 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml @@ -3,10 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:context="http://www.springframework.org/schema/context" - xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> - + @@ -26,8 +25,8 @@ - - @@ -56,9 +55,9 @@ - + - + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml index 8c6194f0a4..a0451bb97e 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml @@ -1,12 +1,20 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml index 81a7c261f9..c802ee7c36 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml @@ -7,8 +7,6 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - - diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml index b70efb607c..2993ee1e46 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml @@ -9,7 +9,11 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - + + + + + diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 6f2d1e2847..ac69429f33 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -3,6 +3,7 @@ ** xref:migration-guides.adoc[] *** xref:migration-guide/migration-guide-2.x-to-3.x.adoc[] *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] +*** xref:migration-guide/migration-guide-4.x-to-5.x.adoc[] * xref:mongodb.adoc[] ** xref:preface.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc new file mode 100644 index 0000000000..4351cc70e7 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -0,0 +1,59 @@ +[[mongodb.migration.4.x-5.x]] += Migration Guide from 4.x to 5.x + +Spring Data MongoDB 4.x requires the MongoDB Java Driver 5.5.x + +To learn more about driver versions please visit the https://www.mongodb.com/docs/drivers/java/sync/current/upgrade/[MongoDB Documentation]. + +== UUID Representation Changes + +Spring Data no longer defaults UUID settings via its configuration support classes, factory beans, nor XML namespace. + +In order to persist UUID values the `UuidRepresentation` hast to be set explicitly. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { + builder.uuidRepresentation(UuidRepresentation.STANDARD); + } + + // ... +} +---- + +XML:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +---- + + + +---- +====== + +== BigInteger/BigDecimal Conversion Changes + +Spring Data no longer defaults BigInteger/BigDecimal conversion via its configuration support classes. +In order to persist those values the default `BigDecimalRepresentation` hast to be set explicitly. + +[source,java] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter configAdapter) { + configAdapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + } + + // ... +} +---- + +Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default to retain previous behaviour. diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc index 5a038dde66..7ddc2bf4ec 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc @@ -4,7 +4,7 @@ include::{commons}@data-commons::page$custom-conversions.adoc[] == Type based Converter The most trivial way of influencing the mapping result is by specifying the desired native MongoDB target type via the `@Field` annotation. -This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in native `org.bson.types.Decimal128` format. +This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in eg. `String` format. .Explicit target type mapping ==== @@ -33,8 +33,7 @@ public class Payment { <1> String _id_ values that represent a valid `ObjectId` are converted automatically. See xref:mongodb/template-crud-operations.adoc#mongo-template.id-handling[How the `_id` Field is Handled in the Mapping Layer] for details. <2> The desired target type is explicitly defined as `String`. -Otherwise, the -`BigDecimal` value would have been turned into a `Decimal128`. +Otherwise. <3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. ==== @@ -113,8 +112,10 @@ To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted v This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc. With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`. -As of Spring Data MongoDB 5.0. the default representation of those types moved to MongoDB native `org.bson.types.Decimal128`. -You can still use the to the previous `String` variant by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING))`. +As of Spring Data MongoDB 5.0. there no longer is a default representation of those types and conversion needs to be configured explicitly. +You can register multiple formats, 1st being default, and still retain the previous behaviour by configuring the `BigDecimalRepresentation` in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128))`. +This allows you to make use of the explicit storage type format via `@Field(targetType = DECIMAL128)` while keeping default conversion set to String. +Choosing none of the provided representations is valid as long as those values are no persisted. [NOTE] ==== diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index 121c80d563..2056dfa20e 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -119,7 +119,7 @@ See xref:mongodb/mapping/custom-conversions.adoc[Custom Conversions - Overriding | native | `{"bin" : { "$binary" : "AQIDBA==", "$type" : "00" }}` -| `java.util.UUID` (Standard UUID) +| `java.util.UUID` (According to UuidRepresentation) | native | `{"uuid" : { "$binary" : "MEaf1CFQ6lSphaa3b9AtlA==", "$type" : "04" }}` @@ -164,13 +164,13 @@ calling `get()` before the actual conversion | `{"value" : "741" }` | `BigInteger` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741) }`, `{"value" : "741" }` | `BigDecimal` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }` | `URL` @@ -200,25 +200,21 @@ calling `get()` before the actual conversion | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `Instant` + -(Joda, JSR310-BackPort) +(Java 8) | converter | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `LocalDate` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T00:00:00.000Z")}` | `LocalDateTime`, `LocalTime` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] -| `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` - -| `DateTime` (Joda) -| converter +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` -| `ZoneId` (Java 8, JSR310-BackPort) +| `ZoneId` (Java 8) | converter | `{"zoneId" : "ECT - Europe/Paris"}`