Skip to content

Commit 12129c4

Browse files
Make sure big number representation allows for backwards compatibility.
Extend configuration options in a way that allows to register different conversion options for numeric values so users can recreate the 4.x default behaviour using String as default while honoring field specific configuration via the targetType attribute of the Field Annotation.
1 parent 1748650 commit 12129c4

File tree

4 files changed

+99
-26
lines changed

4 files changed

+99
-26
lines changed

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public static class MongoConverterConfigurationAdapter {
155155
LocalDateTime.class);
156156

157157
private boolean useNativeDriverJavaTimeCodecs = false;
158-
private @Nullable BigDecimalRepresentation bigDecimals;
158+
private BigDecimalRepresentation @Nullable [] bigDecimals;
159159
private final List<Object> customConverters = new ArrayList<>();
160160

161161
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
@@ -312,14 +312,14 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() {
312312
* Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in
313313
* MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}.
314314
*
315-
* @param representation the representation to use.
315+
* @param representations ordered list of representations to use (first one is default)
316316
* @return this.
317317
* @since 4.5
318318
*/
319-
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) {
319+
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) {
320320

321-
Assert.notNull(representation, "BigDecimalDataType must not be null");
322-
this.bigDecimals = representation;
321+
Assert.notEmpty(representations, "BigDecimalDataType must not be null");
322+
this.bigDecimals = representations;
323323
return this;
324324
}
325325

@@ -375,13 +375,15 @@ ConverterConfiguration createConverterConfiguration() {
375375
List<Object> storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10);
376376

377377
if (bigDecimals != null) {
378-
switch (bigDecimals) {
379-
case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters());
380-
case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters());
378+
for (BigDecimalRepresentation representation : bigDecimals) {
379+
switch (representation) {
380+
case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters());
381+
case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters());
382+
}
381383
}
382384
} else if (LOGGER.isInfoEnabled()) {
383385
LOGGER.info(
384-
"No BigDecimal/BigInteger representation set. Choose [STRING] or [DECIMAL128] to store values in desired format.");
386+
"No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format.");
385387
}
386388

387389
if (useNativeDriverJavaTimeCodecs) {

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

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import static java.time.ZoneId.*;
19-
import static org.mockito.Mockito.*;
20-
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
21-
import static org.springframework.data.mongodb.test.util.Assertions.*;
18+
import static java.time.ZoneId.systemDefault;
19+
import static org.mockito.Mockito.any;
20+
import static org.mockito.Mockito.doReturn;
21+
import static org.mockito.Mockito.eq;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.never;
24+
import static org.mockito.Mockito.spy;
25+
import static org.mockito.Mockito.times;
26+
import static org.mockito.Mockito.verify;
27+
import static org.mockito.Mockito.when;
28+
import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint;
29+
import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument;
30+
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
31+
import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
32+
import static org.springframework.data.mongodb.test.util.Assertions.assertThatThrownBy;
33+
import static org.springframework.data.mongodb.test.util.Assertions.fail;
2234

2335
import java.math.BigDecimal;
2436
import java.math.BigInteger;
@@ -53,7 +65,6 @@
5365
import org.mockito.Mock;
5466
import org.mockito.Mockito;
5567
import org.mockito.junit.jupiter.MockitoExtension;
56-
5768
import org.springframework.aop.framework.ProxyFactory;
5869
import org.springframework.beans.ConversionNotSupportedException;
5970
import org.springframework.beans.factory.annotation.Autowired;
@@ -62,6 +73,7 @@
6273
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
6374
import org.springframework.context.ApplicationContext;
6475
import org.springframework.context.support.StaticApplicationContext;
76+
import org.springframework.core.convert.ConversionFailedException;
6577
import org.springframework.core.convert.ConverterNotFoundException;
6678
import org.springframework.core.convert.converter.Converter;
6779
import org.springframework.data.annotation.Id;
@@ -2176,6 +2188,61 @@ void mapsBigDecimalToDecimal128WhenAnnotatedWithFieldTargetType() {
21762188
assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal));
21772189
}
21782190

2191+
@Test // GH-5037
2192+
@SuppressWarnings("deprecation")
2193+
void mapsBigIntegerToDecimal128WhenAnnotatedWithFieldTargetTypeWhenDefaultConversionIsSetToString() {
2194+
2195+
converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128);
2196+
2197+
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
2198+
source.bigDecimal = BigDecimal.valueOf(3.14159D);
2199+
2200+
org.bson.Document target = new org.bson.Document();
2201+
converter.write(source, target);
2202+
2203+
assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal));
2204+
}
2205+
2206+
@Test // GH-5037
2207+
@SuppressWarnings("deprecation")
2208+
void mapsBigIntegerToStringWhenNotAnnotatedWithFieldTargetTypeAndDefaultConversionIsSetToString() {
2209+
2210+
converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128);
2211+
2212+
BigDecimalContainer source = new BigDecimalContainer();
2213+
source.value = BigDecimal.valueOf(3.14159D);
2214+
org.bson.Document target = new org.bson.Document();
2215+
converter.write(source, target);
2216+
assertThat(target.get("value")).isInstanceOf(String.class);
2217+
}
2218+
2219+
@Test // GH-5037
2220+
void mapsBigIntegerToStringWhenAnnotatedWithFieldTargetTypeEvenWhenDefaultConverterIsSetToDecimal128() {
2221+
2222+
converter = createConverter(BigDecimalRepresentation.DECIMAL128);
2223+
2224+
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
2225+
source.bigIntegerAsString = BigInteger.TWO;
2226+
2227+
org.bson.Document target = new org.bson.Document();
2228+
converter.write(source, target);
2229+
2230+
assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString());
2231+
}
2232+
2233+
@Test // GH-5037
2234+
void explicitBigNumberConversionErrorsIfConverterNotRegistered() {
2235+
2236+
converter = createConverter(BigDecimalRepresentation.STRING);
2237+
2238+
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
2239+
source.bigInteger = BigInteger.TWO;
2240+
2241+
org.bson.Document target = new org.bson.Document();
2242+
2243+
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> converter.write(source, target));
2244+
}
2245+
21792246
@Test // DATAMONGO-2328
21802247
void mapsDateToLongWhenAnnotatedWithFieldTargetType() {
21812248

@@ -3198,7 +3265,6 @@ void beanConverter() {
31983265
return nativeValue.getString("bar");
31993266
}
32003267

3201-
32023268
@Override
32033269
public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) {
32043270
return new org.bson.Document("bar", domainValue);
@@ -3435,7 +3501,7 @@ void usesStringNumericFormat() {
34353501
}
34363502

34373503
private MappingMongoConverter createConverter(
3438-
MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) {
3504+
MongoCustomConversions.BigDecimalRepresentation... bigDecimalRepresentation) {
34393505

34403506
MongoCustomConversions conversions = MongoCustomConversions.create(
34413507
it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation));
@@ -4108,7 +4174,11 @@ static class WithExplicitTargetTypes {
41084174
@Field(targetType = FieldType.DECIMAL128) //
41094175
BigDecimal bigDecimal;
41104176

4111-
@Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger;
4177+
@Field(targetType = FieldType.DECIMAL128) //
4178+
BigInteger bigInteger;
4179+
4180+
@Field(targetType = FieldType.STRING) //
4181+
BigInteger bigIntegerAsString;
41124182

41134183
@Field(targetType = FieldType.INT64) //
41144184
Date dateAsLong;
@@ -4238,7 +4308,6 @@ public SubTypeOfGenericType convert(org.bson.Document source) {
42384308
@WritingConverter
42394309
static class TypeImplementingMapToDocumentConverter implements Converter<TypeImplementingMap, org.bson.Document> {
42404310

4241-
42424311
@Override
42434312
public org.bson.@Nullable Document convert(TypeImplementingMap source) {
42444313
return new org.bson.Document("1st", source.val1).append("2nd", source.val2);
@@ -4440,7 +4509,6 @@ enum Converter2 implements MongoValueConverter<String, org.bson.Document> {
44404509
return value.getString("bar");
44414510
}
44424511

4443-
44444512
@Override
44454513
public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
44464514
return new org.bson.Document("bar", value);
@@ -4454,7 +4522,6 @@ static class Converter1 implements MongoValueConverter<String, org.bson.Document
44544522
return value.getString("foo");
44554523
}
44564524

4457-
44584525
@Override
44594526
public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
44604527
return new org.bson.Document("foo", value);

src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ XML::
4040
== BigInteger/BigDecimal Conversion Changes
4141

4242
Spring Data no longer defaults BigInteger/BigDecimal conversion via its configuration support classes.
43-
In order to persist those values the `BigDecimalRepresentation` hast to be set explicitly.
43+
In order to persist those values the default `BigDecimalRepresentation` hast to be set explicitly.
4444

4545
[source,java]
4646
----
@@ -55,3 +55,6 @@ static class Config extends AbstractMongoClientConfiguration {
5555
// ...
5656
}
5757
----
58+
59+
Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default.
60+
Those using`@Field(targetType = FieldType.DECIMAL128)` need to define a combination of representations `configAdapter.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128)` to set defaulting to String while having the `DECIMAL128` converter being registered for usage with explicit target type configuration.

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ include::{commons}@data-commons::page$custom-conversions.adoc[]
44
== Type based Converter
55

66
The most trivial way of influencing the mapping result is by specifying the desired native MongoDB target type via the `@Field` annotation.
7-
This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in native `org.bson.types.Decimal128` format.
7+
This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in eg. `String` format.
88

99
.Explicit target type mapping
1010
====
@@ -33,8 +33,7 @@ public class Payment {
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.
3535
<2> The desired target type is explicitly defined as `String`.
36-
Otherwise, the
37-
`BigDecimal` value would have been turned into a `Decimal128`.
36+
Otherwise.
3837
<3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`.
3938
====
4039

@@ -113,8 +112,10 @@ To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted v
113112
This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc.
114113

115114
With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`.
116-
As of Spring Data MongoDB 5.0. the default representation of those types moved to MongoDB native `org.bson.types.Decimal128`.
117-
You can still use the to the previous `String` variant by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING))`.
115+
As of Spring Data MongoDB 5.0. there no longer is a default representation of those types and conversion needs to be configured explicitly.
116+
You can register multiple formats, 1st being default, and still retain the previous behaviour by configuring the `BigDecimalRepresentation` in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128))`.
117+
This allows you to make use of the explicit storage type format via `@Field(targetType = DECIMAL128)` while keeping default conversion set to String.
118+
Choosing none of the provided representations is valid as long as those values are no persisted.
118119

119120
[NOTE]
120121
====

0 commit comments

Comments
 (0)