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:
committed by
Mark Paluch
parent
f13d826a69
commit
2b6730d1da
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user