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 86e01afc2..839f49c7d 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 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { Class 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 9a658c44b..31b936585 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.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; @@ -91,12 +90,9 @@ abstract class MongoConverters { 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 @@ abstract class MongoConverters { 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 @@ abstract class MongoConverters { } } + @WritingConverter enum BigIntegerToStringConverter implements Converter { INSTANCE; @@ -220,6 +218,7 @@ abstract class MongoConverters { } } + @ReadingConverter enum StringToBigIntegerConverter implements Converter { INSTANCE; @@ -414,6 +413,17 @@ 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 53ffaedca..3ea7ab997 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.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; @@ -45,6 +47,11 @@ import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.SimplePropertyValueConversions; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; +import org.springframework.data.mongodb.core.mapping.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 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 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 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; + return this; + } /** * Optionally set the {@link PropertyValueConversions} to be applied during mapping. *

@@ -347,15 +366,24 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus 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 b5d1f72e1..53ba040c5 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.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; @@ -3360,7 +3361,47 @@ class MappingMongoConverterUnitTests { 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 @@ class MappingMongoConverterUnitTests { @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 c929fe2ad..3f26b07a6 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.