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.

Closes: #3444
Original pull request: #4916
This commit is contained in:
Christoph Strobl
2025-03-07 17:05:19 +01:00
committed by Mark Paluch
parent f13d826a69
commit 2b6730d1da
5 changed files with 100 additions and 9 deletions

View File

@@ -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;
}

View File

@@ -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<Object> 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<BigInteger, String> {
INSTANCE;
@@ -220,6 +218,7 @@ abstract class MongoConverters {
}
}
@ReadingConverter
enum StringToBigIntegerConverter implements Converter<String, BigInteger> {
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);
}

View File

@@ -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<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class);
private boolean useNativeDriverJavaTimeCodecs = false;
private String numericFormat;
private final List<Object> 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.
* <p>
@@ -347,15 +366,24 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
svc.init();
}
List<Object> 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<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 3);
converters.add(DateToUtcLocalDateConverter.INSTANCE);
converters.add(DateToUtcLocalTimeConverter.INSTANCE);
converters.add(DateToUtcLocalDateTimeConverter.INSTANCE);

View File

@@ -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;

View File

@@ -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.