Skip to content

Commit 2b6730d

Browse files
christophstroblmp911de
authored andcommitted
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
1 parent f13d826 commit 2b6730d

File tree

5 files changed

+100
-9
lines changed

5 files changed

+100
-9
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty
185185
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
186186
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
187187

188-
if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class) {
188+
189+
if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) {
189190
targetType = rawTargetType;
190191
}
191192

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java

+16-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import static org.springframework.data.convert.ConverterBuilder.*;
18+
import static org.springframework.data.convert.ConverterBuilder.reading;
1919

2020
import java.math.BigDecimal;
2121
import java.math.BigInteger;
@@ -47,7 +47,6 @@
4747
import org.bson.types.Code;
4848
import org.bson.types.Decimal128;
4949
import org.bson.types.ObjectId;
50-
5150
import org.springframework.core.convert.ConversionFailedException;
5251
import org.springframework.core.convert.TypeDescriptor;
5352
import org.springframework.core.convert.converter.ConditionalConverter;
@@ -91,12 +90,9 @@ static Collection<Object> getConvertersToRegister() {
9190

9291
List<Object> converters = new ArrayList<>();
9392

94-
converters.add(BigDecimalToStringConverter.INSTANCE);
9593
converters.add(BigDecimalToDecimal128Converter.INSTANCE);
96-
converters.add(StringToBigDecimalConverter.INSTANCE);
9794
converters.add(Decimal128ToBigDecimalConverter.INSTANCE);
98-
converters.add(BigIntegerToStringConverter.INSTANCE);
99-
converters.add(StringToBigIntegerConverter.INSTANCE);
95+
10096
converters.add(URLToStringConverter.INSTANCE);
10197
converters.add(StringToURLConverter.INSTANCE);
10298
converters.add(DocumentToStringConverter.INSTANCE);
@@ -111,6 +107,7 @@ static Collection<Object> getConvertersToRegister() {
111107
converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
112108
converters.add(BinaryToByteArrayConverter.INSTANCE);
113109
converters.add(BsonTimestampToInstantConverter.INSTANCE);
110+
converters.add(NumberToNumberConverterFactory.INSTANCE);
114111

115112
converters.add(VectorToBsonArrayConverter.INSTANCE);
116113
converters.add(ListToVectorConverter.INSTANCE);
@@ -212,6 +209,7 @@ public BigDecimal convert(Decimal128 source) {
212209
}
213210
}
214211

212+
@WritingConverter
215213
enum BigIntegerToStringConverter implements Converter<BigInteger, String> {
216214
INSTANCE;
217215

@@ -220,6 +218,7 @@ public String convert(BigInteger source) {
220218
}
221219
}
222220

221+
@ReadingConverter
223222
enum StringToBigIntegerConverter implements Converter<String, BigInteger> {
224223
INSTANCE;
225224

@@ -414,6 +413,17 @@ public NumberToNumberConverter(Class<T> targetType) {
414413
@Override
415414
public T convert(Number source) {
416415

416+
if (targetType == Decimal128.class) {
417+
418+
if (source instanceof BigDecimal bigDecimal) {
419+
return targetType.cast(BigDecimalToDecimal128Converter.INSTANCE.convert(bigDecimal));
420+
}
421+
422+
if (source instanceof BigInteger bigInteger) {
423+
return targetType.cast(new Decimal128(bigInteger.longValueExact()));
424+
}
425+
}
426+
417427
if (source instanceof AtomicInteger atomicInteger) {
418428
return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType);
419429
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java

+30-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import org.springframework.core.convert.converter.Converter;
3737
import org.springframework.core.convert.converter.ConverterFactory;
3838
import org.springframework.core.convert.converter.GenericConverter;
39+
import org.springframework.core.env.Environment;
40+
import org.springframework.core.env.StandardEnvironment;
3941
import org.springframework.data.convert.ConverterBuilder;
4042
import org.springframework.data.convert.PropertyValueConversions;
4143
import org.springframework.data.convert.PropertyValueConverter;
@@ -45,6 +47,11 @@
4547
import org.springframework.data.convert.SimplePropertyValueConversions;
4648
import org.springframework.data.convert.WritingConverter;
4749
import org.springframework.data.mapping.model.SimpleTypeHolder;
50+
import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter;
51+
import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter;
52+
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
53+
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
54+
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
4855
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
4956
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
5057
import org.springframework.lang.Nullable;
@@ -154,11 +161,18 @@ public static class MongoConverterConfigurationAdapter {
154161
private static final Set<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class);
155162

156163
private boolean useNativeDriverJavaTimeCodecs = false;
164+
private String numericFormat;
157165
private final List<Object> customConverters = new ArrayList<>();
158166

159167
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
160168
private PropertyValueConversions propertyValueConversions = internalValueConversion;
161169

170+
{
171+
Environment env = new StandardEnvironment();
172+
boolean flagPresent = env.containsProperty("mongo.numeric.format");
173+
numericFormat = flagPresent ? env.getProperty("mongo.numeric.format", String.class, "string") : "string";
174+
}
175+
162176
/**
163177
* Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for
164178
* JSR-310 types.
@@ -298,6 +312,11 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
298312
return useNativeDriverJavaTimeCodecs(false);
299313
}
300314

315+
// TODO: might just be a flag like the time codec?
316+
public MongoConverterConfigurationAdapter numericFormat(String format) {
317+
this.numericFormat = format;
318+
return this;
319+
}
301320
/**
302321
* Optionally set the {@link PropertyValueConversions} to be applied during mapping.
303322
* <p>
@@ -347,15 +366,24 @@ ConverterConfiguration createConverterConfiguration() {
347366
svc.init();
348367
}
349368

369+
List<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 7);
370+
if(numericFormat.equals("string")) {
371+
converters.add(BigDecimalToStringConverter.INSTANCE);
372+
converters.add(StringToBigDecimalConverter.INSTANCE);
373+
converters.add(BigIntegerToStringConverter.INSTANCE);
374+
converters.add(StringToBigIntegerConverter.INSTANCE);
375+
}
376+
350377
if (!useNativeDriverJavaTimeCodecs) {
351-
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters, convertiblePair -> true,
378+
379+
converters.addAll(customConverters);
380+
return new ConverterConfiguration(STORE_CONVERSIONS, converters, convertiblePair -> true,
352381
this.propertyValueConversions);
353382
}
354383

355384
/*
356385
* We need to have those converters using UTC as the default ones would go on with the systemDefault.
357386
*/
358-
List<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 3);
359387
converters.add(DateToUtcLocalDateConverter.INSTANCE);
360388
converters.add(DateToUtcLocalTimeConverter.INSTANCE);
361389
converters.add(DateToUtcLocalDateTimeConverter.INSTANCE);

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

+44
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.junit.jupiter.params.provider.Arguments;
4848
import org.junit.jupiter.params.provider.MethodSource;
4949
import org.junit.jupiter.params.provider.ValueSource;
50+
import org.junitpioneer.jupiter.SetSystemProperty;
5051
import org.mockito.Mock;
5152
import org.mockito.Mockito;
5253
import org.mockito.junit.jupiter.MockitoExtension;
@@ -3360,7 +3361,47 @@ void writesByteArrayAsIsIfNoFieldInstructionsGiven() {
33603361
converter.write(source, target);
33613362

33623363
assertThat(target.get("arrayOfPrimitiveBytes", byte[].class)).isSameAs(source.arrayOfPrimitiveBytes);
3364+
}
3365+
3366+
@Test // GH-3444
3367+
void convertsBigIntegerToDecimal128IfFieldTypeIndicatesConversion() {
3368+
3369+
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
3370+
source.bigInteger = BigInteger.valueOf(101);
3371+
3372+
org.bson.Document target = new org.bson.Document();
3373+
converter.write(source, target);
3374+
3375+
assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValueExact()));
3376+
}
3377+
3378+
@Test // GH-3444
3379+
@SetSystemProperty(key = "mongo.numeric.format", value = "decimal128")
3380+
void usesConfiguredNumericFormat() {
3381+
3382+
MongoCustomConversions conversions = new MongoCustomConversions(
3383+
Arrays.asList(new ByteBufferToDoubleHolderConverter()));
3384+
3385+
MongoMappingContext mappingContext = new MongoMappingContext();
3386+
mappingContext.setApplicationContext(context);
3387+
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
3388+
mappingContext.afterPropertiesSet();
3389+
3390+
mappingContext.getPersistentEntity(Address.class);
3391+
3392+
MappingMongoConverter converter = new MappingMongoConverter(resolver, mappingContext);
3393+
converter.setCustomConversions(conversions);
3394+
converter.afterPropertiesSet();
33633395

3396+
BigDecimalContainer container = new BigDecimalContainer();
3397+
container.value = BigDecimal.valueOf(2.5d);
3398+
container.map = Collections.singletonMap("foo", container.value);
3399+
3400+
org.bson.Document document = new org.bson.Document();
3401+
converter.write(container, document);
3402+
3403+
assertThat(document.get("value")).isInstanceOf(Decimal128.class);
3404+
assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class);
33643405
}
33653406

33663407
org.bson.Document write(Object source) {
@@ -4017,6 +4058,9 @@ static class WithExplicitTargetTypes {
40174058
@Field(targetType = FieldType.DECIMAL128) //
40184059
BigDecimal bigDecimal;
40194060

4061+
@Field(targetType = FieldType.DECIMAL128)
4062+
BigInteger bigInteger;
4063+
40204064
@Field(targetType = FieldType.INT64) //
40214065
Date dateAsLong;
40224066

src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc

+8
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,11 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration {
103103
}
104104
}
105105
----
106+
107+
[[mongo.numeric-conversion]]
108+
== Big Number Format
109+
110+
MongoDB in its early days did not have support for large numeric values such as `BigDecimal`.
111+
In order to persist values those types got converted into their `String` representation.
112+
Nowadays `org.bson.types.Decimal128` offers a native solution to storing big numbers.
113+
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.

0 commit comments

Comments
 (0)