Skip to content

Commit 31a3b21

Browse files
committed
Polishing.
Refine documentation. Simplify NumberToNumberConverter. Replace Environment-based configuration with config API. See: #3444 Original pull request: #4916
1 parent 2b6730d commit 31a3b21

File tree

5 files changed

+105
-53
lines changed

5 files changed

+105
-53
lines changed

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

+14-12
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.reading;
18+
import static org.springframework.data.convert.ConverterBuilder.*;
1919

2020
import java.math.BigDecimal;
2121
import java.math.BigInteger;
@@ -47,6 +47,7 @@
4747
import org.bson.types.Code;
4848
import org.bson.types.Decimal128;
4949
import org.bson.types.ObjectId;
50+
5051
import org.springframework.core.convert.ConversionFailedException;
5152
import org.springframework.core.convert.TypeDescriptor;
5253
import org.springframework.core.convert.converter.ConditionalConverter;
@@ -92,6 +93,7 @@ static Collection<Object> getConvertersToRegister() {
9293

9394
converters.add(BigDecimalToDecimal128Converter.INSTANCE);
9495
converters.add(Decimal128ToBigDecimalConverter.INSTANCE);
96+
converters.add(BigIntegerToDecimal128Converter.INSTANCE);
9597

9698
converters.add(URLToStringConverter.INSTANCE);
9799
converters.add(StringToURLConverter.INSTANCE);
@@ -190,6 +192,17 @@ public Decimal128 convert(BigDecimal source) {
190192
}
191193
}
192194

195+
/**
196+
* @since 5.0
197+
*/
198+
enum BigIntegerToDecimal128Converter implements Converter<BigInteger, Decimal128> {
199+
INSTANCE;
200+
201+
public Decimal128 convert(BigInteger source) {
202+
return new Decimal128(new BigDecimal(source));
203+
}
204+
}
205+
193206
enum StringToBigDecimalConverter implements Converter<String, BigDecimal> {
194207
INSTANCE;
195208

@@ -413,17 +426,6 @@ public NumberToNumberConverter(Class<T> targetType) {
413426
@Override
414427
public T convert(Number source) {
415428

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-
427429
if (source instanceof AtomicInteger atomicInteger) {
428430
return NumberUtils.convertNumberToTargetClass(atomicInteger.get(), this.targetType);
429431
}

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

+37-14
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
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;
4139
import org.springframework.data.convert.ConverterBuilder;
4240
import org.springframework.data.convert.PropertyValueConversions;
4341
import org.springframework.data.convert.PropertyValueConverter;
@@ -51,7 +49,6 @@
5149
import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter;
5250
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
5351
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
54-
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
5552
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
5653
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
5754
import org.springframework.lang.Nullable;
@@ -161,18 +158,12 @@ public static class MongoConverterConfigurationAdapter {
161158
private static final Set<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class);
162159

163160
private boolean useNativeDriverJavaTimeCodecs = false;
164-
private String numericFormat;
161+
private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.STRING;
165162
private final List<Object> customConverters = new ArrayList<>();
166163

167164
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
168165
private PropertyValueConversions propertyValueConversions = internalValueConversion;
169166

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-
176167
/**
177168
* Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for
178169
* JSR-310 types.
@@ -312,9 +303,18 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
312303
return useNativeDriverJavaTimeCodecs(false);
313304
}
314305

315-
// TODO: might just be a flag like the time codec?
316-
public MongoConverterConfigurationAdapter numericFormat(String format) {
317-
this.numericFormat = format;
306+
/**
307+
* Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in
308+
* MongoDB. Defaults to {@link BigDecimalRepresentation#STRING}.
309+
*
310+
* @param representation the representation to use.
311+
* @return this.
312+
* @since 4.5
313+
*/
314+
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) {
315+
316+
Assert.notNull(representation, "BigDecimalDataType must not be null");
317+
this.bigDecimals = representation;
318318
return this;
319319
}
320320
/**
@@ -367,7 +367,9 @@ ConverterConfiguration createConverterConfiguration() {
367367
}
368368

369369
List<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 7);
370-
if(numericFormat.equals("string")) {
370+
371+
if (bigDecimals == BigDecimalRepresentation.STRING) {
372+
371373
converters.add(BigDecimalToStringConverter.INSTANCE);
372374
converters.add(StringToBigDecimalConverter.INSTANCE);
373375
converters.add(BigIntegerToStringConverter.INSTANCE);
@@ -403,6 +405,7 @@ ConverterConfiguration createConverterConfiguration() {
403405

404406
@ReadingConverter
405407
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
408+
406409
INSTANCE;
407410

408411
@Override
@@ -434,5 +437,25 @@ public LocalDate convert(Date source) {
434437
private boolean hasDefaultPropertyValueConversions() {
435438
return propertyValueConversions == internalValueConversion;
436439
}
440+
441+
}
442+
443+
/**
444+
* Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB.
445+
*
446+
* @since 4.5
447+
*/
448+
public enum BigDecimalRepresentation {
449+
450+
/**
451+
* Store values as {@link Number#toString() String}. Using strings retains precision but does not support range
452+
* queries.
453+
*/
454+
STRING,
455+
456+
/**
457+
* Store numbers using {@link org.bson.types.Decimal128}. Requires MongoDB Server 3.4 or later.
458+
*/
459+
DECIMAL128
437460
}
438461
}

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

+42-16
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
package org.springframework.data.mongodb.core.convert;
1717

1818
import static java.time.ZoneId.*;
19-
import static org.assertj.core.api.Assertions.*;
2019
import static org.mockito.Mockito.*;
2120
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
21+
import static org.springframework.data.mongodb.test.util.Assertions.*;
2222

2323
import java.math.BigDecimal;
2424
import java.math.BigInteger;
@@ -32,6 +32,7 @@
3232
import java.util.function.Function;
3333
import java.util.stream.Stream;
3434

35+
import org.assertj.core.api.Assertions;
3536
import org.assertj.core.data.Percentage;
3637
import org.bson.BsonDouble;
3738
import org.bson.BsonUndefined;
@@ -47,7 +48,6 @@
4748
import org.junit.jupiter.params.provider.Arguments;
4849
import org.junit.jupiter.params.provider.MethodSource;
4950
import org.junit.jupiter.params.provider.ValueSource;
50-
import org.junitpioneer.jupiter.SetSystemProperty;
5151
import org.mockito.Mock;
5252
import org.mockito.Mockito;
5353
import org.mockito.junit.jupiter.MockitoExtension;
@@ -2552,8 +2552,11 @@ void writeUnwrappedTypeWithComplexValue() {
25522552
assertThat(target) //
25532553
.containsEntry("address", new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")) //
25542554
.doesNotContainKey("street") //
2555+
.doesNotContainKey("city"); //
2556+
2557+
// use exact key matching, do not dive into nested documents
2558+
Assertions.assertThat(target) //
25552559
.doesNotContainKey("address.s") //
2556-
.doesNotContainKey("city") //
25572560
.doesNotContainKey("address.city");
25582561
}
25592562

@@ -3376,11 +3379,42 @@ void convertsBigIntegerToDecimal128IfFieldTypeIndicatesConversion() {
33763379
}
33773380

33783381
@Test // GH-3444
3379-
@SetSystemProperty(key = "mongo.numeric.format", value = "decimal128")
3380-
void usesConfiguredNumericFormat() {
3382+
void usesDecimal128NumericFormat() {
33813383

3382-
MongoCustomConversions conversions = new MongoCustomConversions(
3383-
Arrays.asList(new ByteBufferToDoubleHolderConverter()));
3384+
MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.DECIMAL128);
3385+
3386+
BigDecimalContainer container = new BigDecimalContainer();
3387+
container.value = BigDecimal.valueOf(2.5d);
3388+
container.map = Collections.singletonMap("foo", container.value);
3389+
3390+
org.bson.Document document = new org.bson.Document();
3391+
converter.write(container, document);
3392+
3393+
assertThat(document.get("value")).isInstanceOf(Decimal128.class);
3394+
assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class);
3395+
}
3396+
3397+
@Test // GH-3444
3398+
void usesStringNumericFormat() {
3399+
3400+
MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.STRING);
3401+
3402+
BigDecimalContainer container = new BigDecimalContainer();
3403+
container.value = BigDecimal.valueOf(2.5d);
3404+
container.map = Collections.singletonMap("foo", container.value);
3405+
3406+
org.bson.Document document = new org.bson.Document();
3407+
converter.write(container, document);
3408+
3409+
assertThat(document).containsEntry("value", "2.5");
3410+
assertThat(document).containsEntry("map.foo", "2.5");
3411+
}
3412+
3413+
private MappingMongoConverter createConverter(
3414+
MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) {
3415+
3416+
MongoCustomConversions conversions = MongoCustomConversions.create(
3417+
it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation));
33843418

33853419
MongoMappingContext mappingContext = new MongoMappingContext();
33863420
mappingContext.setApplicationContext(context);
@@ -3393,15 +3427,7 @@ void usesConfiguredNumericFormat() {
33933427
converter.setCustomConversions(conversions);
33943428
converter.afterPropertiesSet();
33953429

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);
3430+
return converter;
34053431
}
34063432

34073433
org.bson.Document write(Object source) {

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ public class Payment {
2626
----
2727
{
2828
"_id" : ObjectId("5ca4a34fa264a01503b36af8"), <1>
29-
"value" : NumberDecimal(2.099), <2>
30-
"date" : ISODate("2019-04-03T12:11:01.870Z") <3>
29+
"value" : NumberDecimal(2.099), <2>
30+
"date" : ISODate("2019-04-03T12:11:01.870Z") <3>
3131
}
3232
----
3333
<1> String _id_ values that represent a valid `ObjectId` are converted automatically. See xref:mongodb/template-crud-operations.adoc#mongo-template.id-handling[How the `_id` Field is Handled in the Mapping Layer]
3434
for details.
35-
<2> The desired target type is explicitly defined as `Decimal128` which translates to `NumberDecimal`. Otherwise the
35+
<2> The desired target type is explicitly defined as `Decimal128` which translates to `NumberDecimal`.
36+
Otherwise, the
3637
`BigDecimal` value would have been truned into a `String`.
37-
<3> `Date` values are handled by the MongoDB driver itself an are stored as `ISODate`.
38+
<3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`.
3839
====
3940

4041
The snippet above is handy for providing simple type hints. To gain more fine-grained control over the mapping process,
@@ -108,6 +109,6 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration {
108109
== Big Number Format
109110

110111
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.
112+
To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted values their `String` representation.
113+
With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`.
114+
You can use the to the native representation by either annotating your properties with `@Field(targetType=DECIMAL128)` or by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(…))`.

src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc

+4-4
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,13 @@ calling `get()` before the actual conversion
165165
166166
| `BigInteger`
167167
| converter +
168-
`String`
169-
| `{"value" : "741" }`
168+
`NumberDecimal`, `String`
169+
| `{"value" : NumberDecimal(741) }`, `{"value" : "741" }`
170170
171171
| `BigDecimal`
172172
| converter +
173-
`String`
174-
| `{"value" : "741.99" }`
173+
`NumberDecimal`, `String`
174+
| `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }`
175175
176176
| `URL`
177177
| converter

0 commit comments

Comments
 (0)