From 8b807890acbce76abdc580e0dbf9cab8a09dd98a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 5 Sep 2025 09:56:06 +0200 Subject: [PATCH] Require explicit BigInteger/BigDecimal conversion settings. See: #5037 Original Pull Request: #5051 --- .../mongodb/core/convert/MongoConverters.java | 5 +- .../core/convert/MongoCustomConversions.java | 36 +++-- .../core/MongoTemplateCollationTests.java | 2 + .../data/mongodb/core/MongoTemplateTests.java | 134 ++++++++++++----- .../MappingMongoConverterUnitTests.java | 139 ++++++++++++++++-- .../core/convert/QueryMapperUnitTests.java | 22 ++- .../test/util/MongoConverterConfigurer.java | 6 + src/main/antora/modules/ROOT/nav.adoc | 1 + .../migration-guide-4.x-to-5.x.adoc | 60 ++++++++ .../mongodb/mapping/custom-conversions.adoc | 11 +- 10 files changed, 339 insertions(+), 77 deletions(-) create mode 100644 src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc 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 36a335cce..2a8c8042a 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 @@ -132,9 +132,7 @@ abstract class MongoConverters { List converters = new ArrayList<>(4); converters.add(BigDecimalToStringConverter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); return converters; } @@ -169,6 +167,9 @@ abstract class MongoConverters { 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)); 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 a54619155..28bc09288 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 @@ -31,8 +31,9 @@ import java.util.Locale; import java.util.Set; import java.util.function.Consumer; +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; @@ -63,6 +64,7 @@ import org.springframework.util.Assert; */ public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions { + private static final Log LOGGER = LogFactory.getLog(MongoCustomConversions.class); private static final List STORE_CONVERTERS; static { @@ -153,7 +155,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; - private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128; + private BigDecimalRepresentation @Nullable [] bigDecimals; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); @@ -310,14 +312,14 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus * Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in * MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}. * - * @param representation the representation to use. + * @param representations ordered list of representations to use (first one is default) * @return this. * @since 4.5 */ - public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) { + public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) { - Assert.notNull(representation, "BigDecimalDataType must not be null"); - this.bigDecimals = representation; + Assert.notEmpty(representations, "BigDecimalDataType must not be null"); + this.bigDecimals = representations; return this; } @@ -372,12 +374,16 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); - if (bigDecimals == BigDecimalRepresentation.STRING) { - storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); - } - - if (bigDecimals == BigDecimalRepresentation.DECIMAL128) { - storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + if (bigDecimals != null) { + for (BigDecimalRepresentation representation : bigDecimals) { + switch (representation) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + } + } + } else if (LOGGER.isInfoEnabled()) { + LOGGER.info( + "No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format."); } if (useNativeDriverJavaTimeCodecs) { @@ -395,9 +401,9 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus // Avoid default registrations - return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) - || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); - }, this.propertyValueConversions); + return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) + || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); + }, this.propertyValueConversions); } 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 bbb3c0eae..5707772fc 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.junit.jupiter.api.extension.ExtendWith; 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 362044f35..9146321e0 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.Instant; 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.aggregation.StringOperators; 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 class MongoTemplateTests { .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 class MongoTemplateTests { } @Test // DATAMONGO-602, GH-4920 - public void testUsingAnInQueryWithBigIntegerId() throws Exception { + public void testUsingAnInQueryWithBigIntegerId() { template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); @@ -887,6 +913,34 @@ public class MongoTemplateTests { 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 class MongoTemplateTests { 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 class MongoTemplateTests { interface PersonPWA { String getName(); + AdressProjection getAddress(); } @@ -2823,7 +2876,7 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 @@ public class MongoTemplateTests { 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 class MongoTemplateTests { 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 1f0af2851..e3982a6ac 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 @@ -15,10 +15,22 @@ */ package org.springframework.data.mongodb.core.convert; -import static java.time.ZoneId.*; -import static org.mockito.Mockito.*; -import static org.springframework.data.mongodb.core.DocumentTestUtils.*; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static java.time.ZoneId.systemDefault; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint; +import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatThrownBy; +import static org.springframework.data.mongodb.test.util.Assertions.fail; import java.math.BigDecimal; import java.math.BigInteger; @@ -27,7 +39,24 @@ import java.nio.ByteBuffer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -53,7 +82,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +90,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; @@ -90,6 +119,8 @@ import org.springframework.data.mongodb.core.DocumentTestUtils; 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 +166,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); @@ -396,6 +427,31 @@ class MappingMongoConverterUnitTests { assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5")); assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); + } // MappingMongoConverterUnitTests + + @Test // DATACMNS-42, DATAMONGO-171, GH-4920 + void writesClassWithBigDecimalFails() { + + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter()); + + mappingContext = new MongoMappingContext(); + mappingContext.setApplicationContext(context); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + mappingContext.afterPropertiesSet(); + + mappingContext.getPersistentEntity(Address.class); + + converter = new MappingMongoConverter(resolver, mappingContext); + + 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 @@ -2149,6 +2205,61 @@ class MappingMongoConverterUnitTests { assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigIntegerToDecimal128WhenAnnotatedWithFieldTargetTypeWhenDefaultConversionIsSetToString() { + + converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigDecimal = BigDecimal.valueOf(3.14159D); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigIntegerToStringWhenNotAnnotatedWithFieldTargetTypeAndDefaultConversionIsSetToString() { + + converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + + BigDecimalContainer source = new BigDecimalContainer(); + source.value = BigDecimal.valueOf(3.14159D); + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + assertThat(target.get("value")).isInstanceOf(String.class); + } + + @Test // GH-5037 + void mapsBigIntegerToStringWhenAnnotatedWithFieldTargetTypeEvenWhenDefaultConverterIsSetToDecimal128() { + + converter = createConverter(BigDecimalRepresentation.DECIMAL128); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigIntegerAsString = BigInteger.TWO; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString()); + } + + @Test // GH-5037 + void explicitBigNumberConversionErrorsIfConverterNotRegistered() { + + converter = createConverter(BigDecimalRepresentation.STRING); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + + org.bson.Document target = new org.bson.Document(); + + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> converter.write(source, target)); + } + @Test // DATAMONGO-2328 void mapsDateToLongWhenAnnotatedWithFieldTargetType() { @@ -3171,7 +3282,6 @@ class MappingMongoConverterUnitTests { return nativeValue.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); @@ -3408,7 +3518,7 @@ class MappingMongoConverterUnitTests { } private MappingMongoConverter createConverter( - MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { + MongoCustomConversions.BigDecimalRepresentation... bigDecimalRepresentation) { MongoCustomConversions conversions = MongoCustomConversions.create( it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); @@ -4081,7 +4191,11 @@ class MappingMongoConverterUnitTests { @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.INT64) // Date dateAsLong; @@ -4211,7 +4325,6 @@ class MappingMongoConverterUnitTests { @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 +4526,6 @@ class MappingMongoConverterUnitTests { 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 +4539,6 @@ class MappingMongoConverterUnitTests { return value.getString("foo"); } - @Override public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("foo", value); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 8de30fd1f..e95003608 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -55,6 +55,7 @@ import org.springframework.data.mongodb.core.aggregation.EvaluationOperators; import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; 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.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.DBRef; @@ -97,7 +98,7 @@ public class QueryMapperUnitTests { @BeforeEach void beforeEach() { - MongoCustomConversions conversions = new MongoCustomConversions(); + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter().bigDecimal(BigDecimalRepresentation.DECIMAL128)); this.context = new MongoMappingContext(); this.context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); @@ -152,6 +153,25 @@ public class QueryMapperUnitTests { assertThat(result).containsEntry("_id", Decimal128.parse("1")); } + @Test // GH-5037 + void leavesBigIntegerAsIsIfNotConfigured() { + + MongoCustomConversions conversions = new MongoCustomConversions(); + context = new MongoMappingContext(); + context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + + converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); + + mapper = new QueryMapper(converter); + + org.bson.Document document = new org.bson.Document("id", new BigInteger("1")); + + org.bson.Document result = mapper.getMappedObject(document, context.getPersistentEntity(IdWrapper.class)); + assertThat(result).containsEntry("_id", new BigInteger("1")); + } + @Test void handlesObjectIdCapableBigIntegerIdsCorrectly() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java index 44b7ae3e4..97b30b144 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java @@ -16,10 +16,12 @@ package org.springframework.data.mongodb.test.util; import java.util.Arrays; +import java.util.function.Consumer; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; /** * Utility to configure {@link MongoCustomConversions}. @@ -37,4 +39,8 @@ public class MongoConverterConfigurer { public void customConverters(Converter... converters) { customConversions(new MongoCustomConversions(Arrays.asList(converters))); } + + public void customConverters(Consumer configurer) { + customConversions(MongoCustomConversions.create(configurer)); + } } diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 6f2d1e284..ac69429f3 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 000000000..898d70236 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -0,0 +1,60 @@ +[[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. +Those using`@Field(targetType = FieldType.DECIMAL128)` need to define a combination of representations `configAdapter.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128)` to set defaulting to String while having the `DECIMAL128` converter being registered for usage with explicit target type configuration. 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 5a038dde6..7ddc2bf4e 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] ====