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 113c52126..7e0365d45 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 @@ -314,7 +314,7 @@ 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#STRING}. + * MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}. * * @param representation the representation to use. * @return this. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/BigDecimalToStringConvertingTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/BigDecimalToStringConvertingTemplateTests.java new file mode 100644 index 000000000..ec17189bc --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/BigDecimalToStringConvertingTemplateTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.data.mongodb.core.MongoTemplateTests.TypeWithNumbers; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; + +import com.mongodb.client.MongoClient; + +/** + * Tests for {@link MongoTemplate} using string representation of {@link BigInteger} values. + * + * @author Christoph Strobl + */ +@ExtendWith(MongoClientExtension.class) +public class BigDecimalToStringConvertingTemplateTests { + + public static final String DB_NAME = "mongo-template-tests"; + + static @Client MongoClient client; + + @SuppressWarnings("deprecation") MongoTestTemplate template = new MongoTestTemplate(cfg -> { + + cfg.configureDatabaseFactory(it -> { + + it.client(client); + it.defaultDb(DB_NAME); + }); + + cfg.configureConversion(it -> { + it.customConversions( + MongoCustomConversions.create(adapter -> adapter.bigDecimal(BigDecimalRepresentation.STRING))); + }); + + cfg.configureMappingContext(it -> { + it.autocreateIndex(false); + }); + + cfg.configureAuditing(it -> { + it.auditingHandler(ctx -> { + return new IsNewAwareAuditingHandler(PersistentEntities.of(ctx)); + }); + }); + }); + + @AfterEach + public void cleanUp() { + template.flush(); + } + + @Test // DATAMONGO-602 + void testUsingAnInQueryWithBigIntegerId() { + + template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); + + PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger(); + p1.setFirstName("Sven"); + p1.setAge(11); + p1.setId(new BigInteger("2666666666666666665069473312490162649510603601")); + template.insert(p1); + PersonWithIdPropertyOfTypeBigInteger p2 = new PersonWithIdPropertyOfTypeBigInteger(); + p2.setFirstName("Mary"); + p2.setAge(21); + p2.setId(new BigInteger("2666666666666666665069473312490162649510603602")); + template.insert(p2); + PersonWithIdPropertyOfTypeBigInteger p3 = new PersonWithIdPropertyOfTypeBigInteger(); + p3.setFirstName("Ann"); + p3.setAge(31); + p3.setId(new BigInteger("2666666666666666665069473312490162649510603603")); + template.insert(p3); + PersonWithIdPropertyOfTypeBigInteger p4 = new PersonWithIdPropertyOfTypeBigInteger(); + p4.setFirstName("John"); + p4.setAge(41); + p4.setId(new BigInteger("2666666666666666665069473312490162649510603604")); + template.insert(p4); + + Query q1 = new Query(Criteria.where("age").in(11, 21, 41)); + List results1 = template.find(q1, PersonWithIdPropertyOfTypeBigInteger.class); + Query q2 = new Query(Criteria.where("firstName").in("Ann", "Mary")); + List results2 = template.find(q2, PersonWithIdPropertyOfTypeBigInteger.class); + Query q3 = new Query(Criteria.where("id").in(new BigInteger("2666666666666666665069473312490162649510603601"), + new BigInteger("2666666666666666665069473312490162649510603604"))); + List results3 = template.find(q3, PersonWithIdPropertyOfTypeBigInteger.class); + assertThat(results1.size()).isEqualTo(3); + assertThat(results2.size()).isEqualTo(2); + assertThat(results3.size()).isEqualTo(2); + } + + @Test // DATAMONGO-1404 + void updatesBigNumberValueUsingStringComparisonWhenUsingMaxOperator() { + + TypeWithNumbers twn = new TypeWithNumbers(); + + // Note that $max operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. + // Therefore "80" is considered greater than "700" + twn.bigIntegerVal = new BigInteger("600"); + twn.bigDeciamVal = new BigDecimal("700.0"); + + template.save(twn); + + Update update = new Update()// + .max("bigIntegerVal", new BigInteger("70")) // + .max("bigDeciamVal", new BigDecimal("80")) // + ; + + template.updateFirst(query(where("id").is(twn.id)), update, TypeWithNumbers.class); + + TypeWithNumbers loaded = template.find(query(where("id").is(twn.id)), TypeWithNumbers.class).get(0); + assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("70")); + assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("80")); + } + + @Test // DATAMONGO-1404 + void updatesBigNumberValueUsingStringComparisonWhenUsingMinOperator() { + + TypeWithNumbers twn = new TypeWithNumbers(); + + // Note that $max operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. + // Therefore "80" is considered greater than "700" + twn.bigIntegerVal = new BigInteger("80"); + twn.bigDeciamVal = new BigDecimal("90.0"); + + template.save(twn); + + Update update = new Update()// + .min("bigIntegerVal", new BigInteger("700")) // + .min("bigDeciamVal", new BigDecimal("800")) // + ; + + template.updateFirst(query(where("id").is(twn.id)), update, TypeWithNumbers.class); + + TypeWithNumbers loaded = template.find(query(where("id").is(twn.id)), TypeWithNumbers.class).get(0); + assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("700")); + assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("800")); + } + +} 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 8575a7753..362044f35 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 @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; @@ -874,7 +875,7 @@ public class MongoTemplateTests { assertThat(results3.size()).isEqualTo(2); } - @Test // DATAMONGO-602 + @Test // DATAMONGO-602, GH-4920 public void testUsingAnInQueryWithBigIntegerId() throws Exception { template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); @@ -883,33 +884,7 @@ public class MongoTemplateTests { p1.setFirstName("Sven"); p1.setAge(11); p1.setId(new BigInteger("2666666666666666665069473312490162649510603601")); - template.insert(p1); - PersonWithIdPropertyOfTypeBigInteger p2 = new PersonWithIdPropertyOfTypeBigInteger(); - p2.setFirstName("Mary"); - p2.setAge(21); - p2.setId(new BigInteger("2666666666666666665069473312490162649510603602")); - template.insert(p2); - PersonWithIdPropertyOfTypeBigInteger p3 = new PersonWithIdPropertyOfTypeBigInteger(); - p3.setFirstName("Ann"); - p3.setAge(31); - p3.setId(new BigInteger("2666666666666666665069473312490162649510603603")); - template.insert(p3); - PersonWithIdPropertyOfTypeBigInteger p4 = new PersonWithIdPropertyOfTypeBigInteger(); - p4.setFirstName("John"); - p4.setAge(41); - p4.setId(new BigInteger("2666666666666666665069473312490162649510603604")); - template.insert(p4); - - Query q1 = new Query(Criteria.where("age").in(11, 21, 41)); - List results1 = template.find(q1, PersonWithIdPropertyOfTypeBigInteger.class); - Query q2 = new Query(Criteria.where("firstName").in("Ann", "Mary")); - List results2 = template.find(q2, PersonWithIdPropertyOfTypeBigInteger.class); - Query q3 = new Query(Criteria.where("id").in(new BigInteger("2666666666666666665069473312490162649510603601"), - new BigInteger("2666666666666666665069473312490162649510603604"))); - List results3 = template.find(q3, PersonWithIdPropertyOfTypeBigInteger.class); - assertThat(results1.size()).isEqualTo(3); - assertThat(results2.size()).isEqualTo(2); - assertThat(results3.size()).isEqualTo(2); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1)); } @Test @@ -3274,7 +3249,6 @@ public class MongoTemplateTests { twn.intVal = 400; twn.longVal = 500L; - // Note that $min operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. twn.bigIntegerVal = new BigInteger("600"); twn.bigDeciamVal = new BigDecimal("700.0"); @@ -3330,7 +3304,6 @@ public class MongoTemplateTests { twn.intVal = 400; twn.longVal = 500L; - // Note that $max operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. twn.bigIntegerVal = new BigInteger("600"); twn.bigDeciamVal = new BigDecimal("700.0"); @@ -3359,13 +3332,11 @@ public class MongoTemplateTests { assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("790")); } - @Test // DATAMONGO-1404 - public void updatesBigNumberValueUsingStringComparisonWhenUsingMaxOperator() { + @Test // DATAMONGO-1404, GH-4920 + public void updatesBigNumberValueUsingUsingMaxOperator() { TypeWithNumbers twn = new TypeWithNumbers(); - // Note that $max operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. - // Therefore "80" is considered greater than "700" twn.bigIntegerVal = new BigInteger("600"); twn.bigDeciamVal = new BigDecimal("700.0"); @@ -3379,17 +3350,15 @@ public class MongoTemplateTests { template.updateFirst(query(where("id").is(twn.id)), update, TypeWithNumbers.class); TypeWithNumbers loaded = template.find(query(where("id").is(twn.id)), TypeWithNumbers.class).get(0); - assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("70")); - assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("80")); + assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("600")); + assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("700.0")); } - @Test // DATAMONGO-1404 - public void updatesBigNumberValueUsingStringComparisonWhenUsingMinOperator() { + @Test // DATAMONGO-1404, GH-4920 + public void updatesBigNumberValueWhenUsingMinOperator() { TypeWithNumbers twn = new TypeWithNumbers(); - // Note that $max operator uses String comparison for BigDecimal/BigInteger comparison according to BSON sort rules. - // Therefore "80" is considered greater than "700" twn.bigIntegerVal = new BigInteger("80"); twn.bigDeciamVal = new BigDecimal("90.0"); @@ -3403,8 +3372,8 @@ public class MongoTemplateTests { template.updateFirst(query(where("id").is(twn.id)), update, TypeWithNumbers.class); TypeWithNumbers loaded = template.find(query(where("id").is(twn.id)), TypeWithNumbers.class).get(0); - assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("700")); - assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("800")); + assertThat(loaded.bigIntegerVal).isEqualTo(new BigInteger("80")); + assertThat(loaded.bigDeciamVal).isEqualTo(new BigDecimal("90.0")); } @Test // DATAMONGO-1431, DATAMONGO-2323 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index 71c395e82..2628a3a5c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -89,7 +89,9 @@ class DbRefMappingMongoConverterUnitTests { this.dbRefResolver = spy(new DefaultDbRefResolver(dbFactory)); this.mappingContext = new MongoMappingContext(); this.mappingContext.setSimpleTypeHolder(new MongoCustomConversions(Collections.emptyList()).getSimpleTypeHolder()); + this.mappingContext.afterPropertiesSet(); this.converter = new MappingMongoConverter(dbRefResolver, mappingContext); + this.converter.afterPropertiesSet(); } @Test // DATAMONGO-347 @@ -103,7 +105,7 @@ class DbRefMappingMongoConverterUnitTests { assertThat(dbRef.getCollectionName()).isEqualTo("person"); } - @Test // DATAMONGO-657 + @Test // DATAMONGO-657, GH-4920 void convertDocumentWithMapDBRef() { Document mapValDocument = new Document(); @@ -610,7 +612,7 @@ class DbRefMappingMongoConverterUnitTests { verify(converterSpy, never()).bulkReadRefs(anyList()); } - @Test // DATAMONGO-1194 + @Test // DATAMONGO-1194, GH-4920 void shouldBulkFetchMapOfReferences() { MapDBRefVal val1 = new MapDBRefVal(); @@ -642,7 +644,7 @@ class DbRefMappingMongoConverterUnitTests { verify(converterSpy, never()).readRef(Mockito.any(DBRef.class)); } - @Test // DATAMONGO-1194 + @Test // DATAMONGO-1194, GH-4920 void shouldBulkFetchLazyMapOfReferences() { MapDBRefVal val1 = new MapDBRefVal(); 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 6f1c7439c..1f0af2851 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 @@ -384,7 +384,7 @@ class MappingMongoConverterUnitTests { assertThat(nestedMap.get("afield")).isEqualTo(firstLevel); } - @Test // DATACMNS-42, DATAMONGO-171 + @Test // DATACMNS-42, DATAMONGO-171, GH-4920 void writesClassWithBigDecimal() { BigDecimalContainer container = new BigDecimalContainer(); @@ -394,9 +394,8 @@ class MappingMongoConverterUnitTests { org.bson.Document document = new org.bson.Document(); converter.write(container, document); - assertThat(document.get("value")).isInstanceOf(String.class); - assertThat((String) document.get("value")).isEqualTo("2.5"); - assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(String.class); + assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5")); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); } @Test // DATACMNS-42, DATAMONGO-171 @@ -513,7 +512,7 @@ class MappingMongoConverterUnitTests { assertThat(((org.bson.Document) map).keySet()).contains("en_US"); } - @Test + @Test // GH-4920 void writesBigIntegerIdCorrectly() { ClassWithBigIntegerId foo = new ClassWithBigIntegerId(); @@ -522,7 +521,7 @@ class MappingMongoConverterUnitTests { org.bson.Document result = new org.bson.Document(); converter.write(foo, result); - assertThat(result.get("_id")).isInstanceOf(String.class); + assertThat(result.get("_id")).isInstanceOf(Decimal128.class); } @Test 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 72d005538..8de30fd1f 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 @@ -34,6 +34,7 @@ import java.util.regex.Pattern; import org.bson.BsonRegularExpression; import org.bson.conversions.Bson; import org.bson.types.Code; +import org.bson.types.Decimal128; import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,6 +54,7 @@ import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; 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.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.DBRef; @@ -126,13 +128,30 @@ public class QueryMapperUnitTests { } @Test - void handlesBigIntegerIdsCorrectly() { + @SuppressWarnings("deprecation") + void handlesBigIntegerIdsCorrectly/*in legacy string format*/() { + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + converter.setCustomConversions(MongoCustomConversions.create(adapter -> adapter.bigDecimal(BigDecimalRepresentation.STRING))); + converter.afterPropertiesSet(); + + QueryMapper 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", "1"); } + @Test // GH-4920 + void handlesBigIntegerIdAsDecimal128Correctly() { + + 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", Decimal128.parse("1")); + } + @Test void handlesObjectIdCapableBigIntegerIdsCorrectly() { 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 4553be1d4..9c90aafc3 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 @@ -34,7 +34,7 @@ public class Payment { for details. <2> The desired target type is explicitly defined as `Decimal128` which translates to `NumberDecimal`. Otherwise, the -`BigDecimal` value would have been truned into a `String`. +`BigDecimal` value would have been turned into a `String`. <3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. ==== @@ -110,5 +110,12 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration { MongoDB in its early days did not have support for large numeric values such as `BigDecimal`. To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted values their `String` representation. +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`. -You can use the to the native representation by either annotating your properties with `@Field(targetType=DECIMAL128)` or by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(…))`. +As off Spring Data MongoDB 5.0 the default representation of those types moved to MongoDB native `org.bson.types.Decimal128` as well. +You can still use the to the deprecated `String` variant by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING))`. + +[NOTE] +==== +Very large values, though being valid in their java, might exceed the maximum bit length of `org.bson.types.Decimal128` in their store native representation. +====