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 31b936585..f9a67d73a 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.Binary; 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 @@ abstract class MongoConverters { converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); + converters.add(BigIntegerToDecimal128Converter.INSTANCE); converters.add(URLToStringConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE); @@ -190,6 +192,17 @@ abstract class MongoConverters { } } + /** + * @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 @@ abstract class MongoConverters { @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 3ea7ab997..050c3bd27 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.TypeDescriptor; 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.BigDecimalT 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 class MongoCustomConversions extends org.springframework.data.convert.Cus 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 class MongoCustomConversions extends org.springframework.data.convert.Cus 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 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus } 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 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @ReadingConverter private enum DateToUtcLocalDateTimeConverter implements Converter { + INSTANCE; @Override @@ -434,5 +437,25 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus 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 53ba040c5..cf6d69c6c 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.Consumer; 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.ParameterizedTest; 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 @@ class MappingMongoConverterUnitTests { 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 @@ class MappingMongoConverterUnitTests { } @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 @@ class MappingMongoConverterUnitTests { 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 3f26b07a6..4553be1d4 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 411c06f02..d76266c36 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