Browse Source

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
pull/4921/head
Christoph Strobl 10 months ago committed by Mark Paluch
parent
commit
2b6730d1da
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
  2. 22
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
  3. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
  4. 44
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  5. 8
      src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc

3
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<?> rawTargetType = computeTargetType(property); // target type before conversion
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type 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; targetType = rawTargetType;
} }

22
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; 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.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
@ -47,7 +47,6 @@ import org.bson.types.Binary;
import org.bson.types.Code; import org.bson.types.Code;
import org.bson.types.Decimal128; import org.bson.types.Decimal128;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter; import org.springframework.core.convert.converter.ConditionalConverter;
@ -91,12 +90,9 @@ abstract class MongoConverters {
List<Object> converters = new ArrayList<>(); List<Object> converters = new ArrayList<>();
converters.add(BigDecimalToStringConverter.INSTANCE);
converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(BigDecimalToDecimal128Converter.INSTANCE);
converters.add(StringToBigDecimalConverter.INSTANCE);
converters.add(Decimal128ToBigDecimalConverter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE);
converters.add(BigIntegerToStringConverter.INSTANCE);
converters.add(StringToBigIntegerConverter.INSTANCE);
converters.add(URLToStringConverter.INSTANCE); converters.add(URLToStringConverter.INSTANCE);
converters.add(StringToURLConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE);
converters.add(DocumentToStringConverter.INSTANCE); converters.add(DocumentToStringConverter.INSTANCE);
@ -111,6 +107,7 @@ abstract class MongoConverters {
converters.add(IntegerToAtomicIntegerConverter.INSTANCE); converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(BinaryToByteArrayConverter.INSTANCE); converters.add(BinaryToByteArrayConverter.INSTANCE);
converters.add(BsonTimestampToInstantConverter.INSTANCE); converters.add(BsonTimestampToInstantConverter.INSTANCE);
converters.add(NumberToNumberConverterFactory.INSTANCE);
converters.add(VectorToBsonArrayConverter.INSTANCE); converters.add(VectorToBsonArrayConverter.INSTANCE);
converters.add(ListToVectorConverter.INSTANCE); converters.add(ListToVectorConverter.INSTANCE);
@ -212,6 +209,7 @@ abstract class MongoConverters {
} }
} }
@WritingConverter
enum BigIntegerToStringConverter implements Converter<BigInteger, String> { enum BigIntegerToStringConverter implements Converter<BigInteger, String> {
INSTANCE; INSTANCE;
@ -220,6 +218,7 @@ abstract class MongoConverters {
} }
} }
@ReadingConverter
enum StringToBigIntegerConverter implements Converter<String, BigInteger> { enum StringToBigIntegerConverter implements Converter<String, BigInteger> {
INSTANCE; INSTANCE;
@ -414,6 +413,17 @@ abstract class MongoConverters {
@Override @Override
public T convert(Number source) { 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) { if (source instanceof AtomicInteger atomicInteger) {
return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType); return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType);
} }

32
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.Converter;
import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter; 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.ConverterBuilder;
import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConversions;
import org.springframework.data.convert.PropertyValueConverter; 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.SimplePropertyValueConversions;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder; 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.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.lang.Nullable; 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 static final Set<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class);
private boolean useNativeDriverJavaTimeCodecs = false; private boolean useNativeDriverJavaTimeCodecs = false;
private String numericFormat;
private final List<Object> customConverters = new ArrayList<>(); private final List<Object> customConverters = new ArrayList<>();
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
private PropertyValueConversions propertyValueConversions = internalValueConversion; 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 * Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for
* JSR-310 types. * JSR-310 types.
@ -298,6 +312,11 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
return useNativeDriverJavaTimeCodecs(false); 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. * Optionally set the {@link PropertyValueConversions} to be applied during mapping.
* <p> * <p>
@ -347,15 +366,24 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
svc.init(); 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) { if (!useNativeDriverJavaTimeCodecs) {
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters, convertiblePair -> true,
converters.addAll(customConverters);
return new ConverterConfiguration(STORE_CONVERSIONS, converters, convertiblePair -> true,
this.propertyValueConversions); this.propertyValueConversions);
} }
/* /*
* We need to have those converters using UTC as the default ones would go on with the systemDefault. * 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(DateToUtcLocalDateConverter.INSTANCE);
converters.add(DateToUtcLocalTimeConverter.INSTANCE); converters.add(DateToUtcLocalTimeConverter.INSTANCE);
converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); converters.add(DateToUtcLocalDateTimeConverter.INSTANCE);

44
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.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.SetSystemProperty;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@ -3360,7 +3361,47 @@ class MappingMongoConverterUnitTests {
converter.write(source, target); converter.write(source, target);
assertThat(target.get("arrayOfPrimitiveBytes", byte[].class)).isSameAs(source.arrayOfPrimitiveBytes); 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) { org.bson.Document write(Object source) {
@ -4017,6 +4058,9 @@ class MappingMongoConverterUnitTests {
@Field(targetType = FieldType.DECIMAL128) // @Field(targetType = FieldType.DECIMAL128) //
BigDecimal bigDecimal; BigDecimal bigDecimal;
@Field(targetType = FieldType.DECIMAL128)
BigInteger bigInteger;
@Field(targetType = FieldType.INT64) // @Field(targetType = FieldType.INT64) //
Date dateAsLong; Date dateAsLong;

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

Loading…
Cancel
Save