From 2d84a9c01ecc55ae2699a15113db75c986e33e24 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 7 Mar 2025 14:29:16 +0100 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index ded4d85d02..35afe83951 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.x-GH-3444-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..5eaf1a0ff6 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 - 4.5.0-SNAPSHOT + 4.5.x-GH-3444-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 37e68c6f78..4d51aca912 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.x-GH-3444-SNAPSHOT ../pom.xml From d021f310e2e5dec8952e654128b649beb95e4ba2 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 7 Mar 2025 17:05:19 +0100 Subject: [PATCH 2/3] Introduce Configuration for `BigDecimal` representation. We now allow configuring the default representation for BigDecimal and BigInteger values, still defaulting to string. BigDecimal can be configured to use Decimal128 by default. --- .../core/MappingMongoJsonSchemaCreator.java | 3 +- .../mongodb/core/convert/MongoConverters.java | 22 +++++++--- .../core/convert/MongoCustomConversions.java | 32 +++++++++++++- .../MappingMongoConverterUnitTests.java | 44 +++++++++++++++++++ .../mongodb/mapping/custom-conversions.adoc | 8 ++++ 5 files changed, 100 insertions(+), 9 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index 86e01afc26..839f49c7da 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -185,7 +185,8 @@ private JsonSchemaProperty computeSchemaForProperty(List rawTargetType = computeTargetType(property); // target type before conversion Class targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type - if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class) { + + if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) { targetType = rawTargetType; } 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 9a658c44ba..31b936585b 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 @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import static org.springframework.data.convert.ConverterBuilder.*; +import static org.springframework.data.convert.ConverterBuilder.reading; import java.math.BigDecimal; import java.math.BigInteger; @@ -47,7 +47,6 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; - import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalConverter; @@ -91,12 +90,9 @@ static Collection getConvertersToRegister() { List converters = new ArrayList<>(); - converters.add(BigDecimalToStringConverter.INSTANCE); converters.add(BigDecimalToDecimal128Converter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); - converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); + converters.add(URLToStringConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE); converters.add(DocumentToStringConverter.INSTANCE); @@ -111,6 +107,7 @@ static Collection getConvertersToRegister() { converters.add(IntegerToAtomicIntegerConverter.INSTANCE); converters.add(BinaryToByteArrayConverter.INSTANCE); converters.add(BsonTimestampToInstantConverter.INSTANCE); + converters.add(NumberToNumberConverterFactory.INSTANCE); converters.add(VectorToBsonArrayConverter.INSTANCE); converters.add(ListToVectorConverter.INSTANCE); @@ -212,6 +209,7 @@ public BigDecimal convert(Decimal128 source) { } } + @WritingConverter enum BigIntegerToStringConverter implements Converter { INSTANCE; @@ -220,6 +218,7 @@ public String convert(BigInteger source) { } } + @ReadingConverter enum StringToBigIntegerConverter implements Converter { INSTANCE; @@ -414,6 +413,17 @@ public NumberToNumberConverter(Class targetType) { @Override public T convert(Number source) { + if (targetType == Decimal128.class) { + + if (source instanceof BigDecimal bigDecimal) { + return targetType.cast(BigDecimalToDecimal128Converter.INSTANCE.convert(bigDecimal)); + } + + if (source instanceof BigInteger bigInteger) { + return targetType.cast(new Decimal128(bigInteger.longValueExact())); + } + } + if (source instanceof AtomicInteger atomicInteger) { return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType); } 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 53ffaedcab..3ea7ab9977 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 @@ -36,6 +36,8 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConverter; @@ -45,6 +47,11 @@ 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.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.lang.Nullable; @@ -154,11 +161,18 @@ public static class MongoConverterConfigurationAdapter { private static final Set> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; + private String numericFormat; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); private PropertyValueConversions propertyValueConversions = internalValueConversion; + { + Environment env = new StandardEnvironment(); + boolean flagPresent = env.containsProperty("mongo.numeric.format"); + numericFormat = flagPresent ? env.getProperty("mongo.numeric.format", String.class, "string") : "string"; + } + /** * Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for * JSR-310 types. @@ -298,6 +312,11 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(false); } + // TODO: might just be a flag like the time codec? + public MongoConverterConfigurationAdapter numericFormat(String format) { + this.numericFormat = format; + return this; + } /** * Optionally set the {@link PropertyValueConversions} to be applied during mapping. *

@@ -347,15 +366,24 @@ ConverterConfiguration createConverterConfiguration() { svc.init(); } + List converters = new ArrayList<>(STORE_CONVERTERS.size() + 7); + if(numericFormat.equals("string")) { + converters.add(BigDecimalToStringConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converters.add(BigIntegerToStringConverter.INSTANCE); + converters.add(StringToBigIntegerConverter.INSTANCE); + } + if (!useNativeDriverJavaTimeCodecs) { - return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters, convertiblePair -> true, + + 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. */ - List converters = new ArrayList<>(STORE_CONVERTERS.size() + 3); converters.add(DateToUtcLocalDateConverter.INSTANCE); converters.add(DateToUtcLocalTimeConverter.INSTANCE); converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); 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 b5d1f72e1c..53ba040c55 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 @@ -47,6 +47,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.SetSystemProperty; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -3360,7 +3361,47 @@ void writesByteArrayAsIsIfNoFieldInstructionsGiven() { converter.write(source, target); assertThat(target.get("arrayOfPrimitiveBytes", byte[].class)).isSameAs(source.arrayOfPrimitiveBytes); + } + + @Test // GH-3444 + void convertsBigIntegerToDecimal128IfFieldTypeIndicatesConversion() { + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigInteger = BigInteger.valueOf(101); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValueExact())); + } + + @Test // GH-3444 + @SetSystemProperty(key = "mongo.numeric.format", value = "decimal128") + void usesConfiguredNumericFormat() { + + MongoCustomConversions conversions = new MongoCustomConversions( + Arrays.asList(new ByteBufferToDoubleHolderConverter())); + + MongoMappingContext mappingContext = new MongoMappingContext(); + mappingContext.setApplicationContext(context); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + mappingContext.afterPropertiesSet(); + + mappingContext.getPersistentEntity(Address.class); + + MappingMongoConverter converter = new MappingMongoConverter(resolver, mappingContext); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); + 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(Decimal128.class); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); } org.bson.Document write(Object source) { @@ -4017,6 +4058,9 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; + @Field(targetType = FieldType.DECIMAL128) + BigInteger bigInteger; + @Field(targetType = FieldType.INT64) // Date dateAsLong; 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 c929fe2ad4..3f26b07a66 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 @@ -103,3 +103,11 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration { } } ---- + +[[mongo.numeric-conversion]] +== Big Number Format + +MongoDB in its early days did not have support for large numeric values such as `BigDecimal`. +In order to persist values those types got converted into their `String` representation. +Nowadays `org.bson.types.Decimal128` offers a native solution to storing big numbers. +Next to influencing the to be stored numeric representation via the `@Field` annotation you can configure `MongoCustomConversions` to use `Decimal128` instead of `String` via the `MongoConverterConfigurationAdapter#numericFormat(...)` or set the `mongo.numeric.format=decimal128` property. From cbc98d066dc7818df775fa434ccfb972572245ba Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 10 Mar 2025 16:13:58 +0100 Subject: [PATCH 3/3] Polishing. Refine documentation. Simplify NumberToNumberConverter. Replace Environment-based configuration with config API. --- .../mongodb/core/convert/MongoConverters.java | 26 +++++---- .../core/convert/MongoCustomConversions.java | 51 +++++++++++----- .../MappingMongoConverterUnitTests.java | 58 ++++++++++++++----- .../mongodb/mapping/custom-conversions.adoc | 15 ++--- .../ROOT/pages/mongodb/mapping/mapping.adoc | 8 +-- 5 files changed, 105 insertions(+), 53 deletions(-) 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 31b936585b..f9a67d73a0 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 @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import static org.springframework.data.convert.ConverterBuilder.reading; +import static org.springframework.data.convert.ConverterBuilder.*; import java.math.BigDecimal; import java.math.BigInteger; @@ -47,6 +47,7 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; + import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalConverter; @@ -92,6 +93,7 @@ static Collection getConvertersToRegister() { converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); + converters.add(BigIntegerToDecimal128Converter.INSTANCE); converters.add(URLToStringConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE); @@ -190,6 +192,17 @@ public Decimal128 convert(BigDecimal source) { } } + /** + * @since 5.0 + */ + enum BigIntegerToDecimal128Converter implements Converter { + INSTANCE; + + public Decimal128 convert(BigInteger source) { + return new Decimal128(new BigDecimal(source)); + } + } + enum StringToBigDecimalConverter implements Converter { INSTANCE; @@ -413,17 +426,6 @@ public NumberToNumberConverter(Class targetType) { @Override public T convert(Number source) { - if (targetType == Decimal128.class) { - - if (source instanceof BigDecimal bigDecimal) { - return targetType.cast(BigDecimalToDecimal128Converter.INSTANCE.convert(bigDecimal)); - } - - if (source instanceof BigInteger bigInteger) { - return targetType.cast(new Decimal128(bigInteger.longValueExact())); - } - } - if (source instanceof AtomicInteger atomicInteger) { return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType); } 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 3ea7ab9977..050c3bd27d 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 @@ -36,8 +36,6 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConverter; @@ -51,7 +49,6 @@ 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.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.lang.Nullable; @@ -161,18 +158,12 @@ public static class MongoConverterConfigurationAdapter { private static final Set> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; - private String numericFormat; + private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.STRING; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); private PropertyValueConversions propertyValueConversions = internalValueConversion; - { - Environment env = new StandardEnvironment(); - boolean flagPresent = env.containsProperty("mongo.numeric.format"); - numericFormat = flagPresent ? env.getProperty("mongo.numeric.format", String.class, "string") : "string"; - } - /** * Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for * JSR-310 types. @@ -312,9 +303,18 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(false); } - // TODO: might just be a flag like the time codec? - public MongoConverterConfigurationAdapter numericFormat(String format) { - this.numericFormat = format; + /** + * Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in + * MongoDB. Defaults to {@link BigDecimalRepresentation#STRING}. + * + * @param representation the representation to use. + * @return this. + * @since 4.5 + */ + public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) { + + Assert.notNull(representation, "BigDecimalDataType must not be null"); + this.bigDecimals = representation; return this; } /** @@ -367,7 +367,9 @@ ConverterConfiguration createConverterConfiguration() { } List converters = new ArrayList<>(STORE_CONVERTERS.size() + 7); - if(numericFormat.equals("string")) { + + if (bigDecimals == BigDecimalRepresentation.STRING) { + converters.add(BigDecimalToStringConverter.INSTANCE); converters.add(StringToBigDecimalConverter.INSTANCE); converters.add(BigIntegerToStringConverter.INSTANCE); @@ -403,6 +405,7 @@ ConverterConfiguration createConverterConfiguration() { @ReadingConverter private enum DateToUtcLocalDateTimeConverter implements Converter { + INSTANCE; @Override @@ -434,5 +437,25 @@ public LocalDate convert(Date source) { private boolean hasDefaultPropertyValueConversions() { return propertyValueConversions == internalValueConversion; } + + } + + /** + * Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB. + * + * @since 4.5 + */ + public enum BigDecimalRepresentation { + + /** + * Store values as {@link Number#toString() String}. Using strings retains precision but does not support range + * queries. + */ + STRING, + + /** + * Store numbers using {@link org.bson.types.Decimal128}. Requires MongoDB Server 3.4 or later. + */ + DECIMAL128 } } 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 53ba040c55..cf6d69c6c3 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 @@ -16,9 +16,9 @@ package org.springframework.data.mongodb.core.convert; import static java.time.ZoneId.*; -import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.DocumentTestUtils.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import java.math.BigDecimal; import java.math.BigInteger; @@ -32,6 +32,7 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.assertj.core.data.Percentage; import org.bson.BsonDouble; import org.bson.BsonUndefined; @@ -47,7 +48,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.junitpioneer.jupiter.SetSystemProperty; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -2552,8 +2552,11 @@ void writeUnwrappedTypeWithComplexValue() { assertThat(target) // .containsEntry("address", new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")) // .doesNotContainKey("street") // + .doesNotContainKey("city"); // + + // use exact key matching, do not dive into nested documents + Assertions.assertThat(target) // .doesNotContainKey("address.s") // - .doesNotContainKey("city") // .doesNotContainKey("address.city"); } @@ -3376,11 +3379,42 @@ void convertsBigIntegerToDecimal128IfFieldTypeIndicatesConversion() { } @Test // GH-3444 - @SetSystemProperty(key = "mongo.numeric.format", value = "decimal128") - void usesConfiguredNumericFormat() { + void usesDecimal128NumericFormat() { - MongoCustomConversions conversions = new MongoCustomConversions( - Arrays.asList(new ByteBufferToDoubleHolderConverter())); + MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.DECIMAL128); + + 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(Decimal128.class); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); + } + + @Test // GH-3444 + void usesStringNumericFormat() { + + MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.STRING); + + 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).containsEntry("value", "2.5"); + assertThat(document).containsEntry("map.foo", "2.5"); + } + + private MappingMongoConverter createConverter( + MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { + + MongoCustomConversions conversions = MongoCustomConversions.create( + it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); MongoMappingContext mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -3393,15 +3427,7 @@ void usesConfiguredNumericFormat() { converter.setCustomConversions(conversions); converter.afterPropertiesSet(); - 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(Decimal128.class); - assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); + return converter; } org.bson.Document write(Object source) { 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 3f26b07a66..4553be1d43 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 @@ -26,15 +26,16 @@ public class Payment { ---- { "_id" : ObjectId("5ca4a34fa264a01503b36af8"), <1> - "value" : NumberDecimal(2.099), <2> - "date" : ISODate("2019-04-03T12:11:01.870Z") <3> + "value" : NumberDecimal(2.099), <2> + "date" : ISODate("2019-04-03T12:11:01.870Z") <3> } ---- <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 `Decimal128` which translates to `NumberDecimal`. Otherwise the +<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`. -<3> `Date` values are handled by the MongoDB driver itself an are stored as `ISODate`. +<3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. ==== The snippet above is handy for providing simple type hints. To gain more fine-grained control over the mapping process, @@ -108,6 +109,6 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration { == Big Number Format MongoDB in its early days did not have support for large numeric values such as `BigDecimal`. -In order to persist values those types got converted into their `String` representation. -Nowadays `org.bson.types.Decimal128` offers a native solution to storing big numbers. -Next to influencing the to be stored numeric representation via the `@Field` annotation you can configure `MongoCustomConversions` to use `Decimal128` instead of `String` via the `MongoConverterConfigurationAdapter#numericFormat(...)` or set the `mongo.numeric.format=decimal128` property. +To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted values their `String` representation. +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(…))`. 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 411c06f02f..d76266c36a 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -165,13 +165,13 @@ calling `get()` before the actual conversion | `BigInteger` | converter + -`String` -| `{"value" : "741" }` +`NumberDecimal`, `String` +| `{"value" : NumberDecimal(741) }`, `{"value" : "741" }` | `BigDecimal` | converter + -`String` -| `{"value" : "741.99" }` +`NumberDecimal`, `String` +| `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }` | `URL` | converter