Browse Source

Polishing.

Refine documentation. Simplify NumberToNumberConverter. Replace Environment-based configuration with config API.

See: #3444
Original pull request: #4916
pull/4921/head
Mark Paluch 9 months ago
parent
commit
31a3b21aa6
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 26
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
  2. 51
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
  3. 58
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  4. 11
      src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc
  5. 8
      src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc

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

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.core.convert;
import static org.springframework.data.convert.ConverterBuilder.reading;
import static org.springframework.data.convert.ConverterBuilder.*;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -47,6 +47,7 @@ import org.bson.types.Binary; @@ -47,6 +47,7 @@ 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;
@ -92,6 +93,7 @@ abstract class MongoConverters { @@ -92,6 +93,7 @@ abstract class MongoConverters {
converters.add(BigDecimalToDecimal128Converter.INSTANCE);
converters.add(Decimal128ToBigDecimalConverter.INSTANCE);
converters.add(BigIntegerToDecimal128Converter.INSTANCE);
converters.add(URLToStringConverter.INSTANCE);
converters.add(StringToURLConverter.INSTANCE);
@ -190,6 +192,17 @@ abstract class MongoConverters { @@ -190,6 +192,17 @@ abstract class MongoConverters {
}
}
/**
* @since 5.0
*/
enum BigIntegerToDecimal128Converter implements Converter<BigInteger, Decimal128> {
INSTANCE;
public Decimal128 convert(BigInteger source) {
return new Decimal128(new BigDecimal(source));
}
}
enum StringToBigDecimalConverter implements Converter<String, BigDecimal> {
INSTANCE;
@ -413,17 +426,6 @@ abstract class MongoConverters { @@ -413,17 +426,6 @@ 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);
}

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

@ -36,8 +36,6 @@ import org.springframework.core.convert.TypeDescriptor; @@ -36,8 +36,6 @@ 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;
@ -51,7 +49,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalT @@ -51,7 +49,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalT
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;
@ -161,18 +158,12 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -161,18 +158,12 @@ 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 BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.STRING;
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.
@ -312,9 +303,18 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -312,9 +303,18 @@ 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;
/**
* Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in
* MongoDB. Defaults to {@link BigDecimalRepresentation#STRING}.
*
* @param representation the representation to use.
* @return this.
* @since 4.5
*/
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) {
Assert.notNull(representation, "BigDecimalDataType must not be null");
this.bigDecimals = representation;
return this;
}
/**
@ -367,7 +367,9 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -367,7 +367,9 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
}
List<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 7);
if(numericFormat.equals("string")) {
if (bigDecimals == BigDecimalRepresentation.STRING) {
converters.add(BigDecimalToStringConverter.INSTANCE);
converters.add(StringToBigDecimalConverter.INSTANCE);
converters.add(BigIntegerToStringConverter.INSTANCE);
@ -403,6 +405,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -403,6 +405,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
@ReadingConverter
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
INSTANCE;
@Override
@ -434,5 +437,25 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -434,5 +437,25 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
private boolean hasDefaultPropertyValueConversions() {
return propertyValueConversions == internalValueConversion;
}
}
/**
* Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB.
*
* @since 4.5
*/
public enum BigDecimalRepresentation {
/**
* Store values as {@link Number#toString() String}. Using strings retains precision but does not support range
* queries.
*/
STRING,
/**
* Store numbers using {@link org.bson.types.Decimal128}. Requires MongoDB Server 3.4 or later.
*/
DECIMAL128
}
}

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

@ -16,9 +16,9 @@ @@ -16,9 +16,9 @@
package org.springframework.data.mongodb.core.convert;
import static java.time.ZoneId.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -32,6 +32,7 @@ import java.util.function.Consumer; @@ -32,6 +32,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.assertj.core.api.Assertions;
import org.assertj.core.data.Percentage;
import org.bson.BsonDouble;
import org.bson.BsonUndefined;
@ -47,7 +48,6 @@ import org.junit.jupiter.params.ParameterizedTest; @@ -47,7 +48,6 @@ 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;
@ -2552,8 +2552,11 @@ class MappingMongoConverterUnitTests { @@ -2552,8 +2552,11 @@ class MappingMongoConverterUnitTests {
assertThat(target) //
.containsEntry("address", new org.bson.Document("s", "1007 Mountain Drive").append("city", "Gotham")) //
.doesNotContainKey("street") //
.doesNotContainKey("city"); //
// use exact key matching, do not dive into nested documents
Assertions.assertThat(target) //
.doesNotContainKey("address.s") //
.doesNotContainKey("city") //
.doesNotContainKey("address.city");
}
@ -3376,11 +3379,42 @@ class MappingMongoConverterUnitTests { @@ -3376,11 +3379,42 @@ class MappingMongoConverterUnitTests {
}
@Test // GH-3444
@SetSystemProperty(key = "mongo.numeric.format", value = "decimal128")
void usesConfiguredNumericFormat() {
void usesDecimal128NumericFormat() {
MongoCustomConversions conversions = new MongoCustomConversions(
Arrays.asList(new ByteBufferToDoubleHolderConverter()));
MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.DECIMAL128);
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);
}
@Test // GH-3444
void usesStringNumericFormat() {
MappingMongoConverter converter = createConverter(MongoCustomConversions.BigDecimalRepresentation.STRING);
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).containsEntry("value", "2.5");
assertThat(document).containsEntry("map.foo", "2.5");
}
private MappingMongoConverter createConverter(
MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) {
MongoCustomConversions conversions = MongoCustomConversions.create(
it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation));
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(context);
@ -3393,15 +3427,7 @@ class MappingMongoConverterUnitTests { @@ -3393,15 +3427,7 @@ class MappingMongoConverterUnitTests {
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);
return converter;
}
org.bson.Document write(Object source) {

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

@ -32,9 +32,10 @@ public class Payment { @@ -32,9 +32,10 @@ public class Payment {
----
<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]
for details.
<2> The desired target type is explicitly defined as `Decimal128` which translates to `NumberDecimal`. Otherwise the
<2> The desired target type is explicitly defined as `Decimal128` which translates to `NumberDecimal`.
Otherwise, the
`BigDecimal` value would have been truned into a `String`.
<3> `Date` values are handled by the MongoDB driver itself an are stored as `ISODate`.
<3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`.
====
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 { @@ -108,6 +109,6 @@ class MyMongoConfiguration extends AbstractMongoClientConfiguration {
== 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.
To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted values their `String` representation.
With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`.
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(…))`.

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

@ -165,13 +165,13 @@ calling `get()` before the actual conversion @@ -165,13 +165,13 @@ calling `get()` before the actual conversion
| `BigInteger`
| converter +
`String`
| `{"value" : "741" }`
`NumberDecimal`, `String`
| `{"value" : NumberDecimal(741) }`, `{"value" : "741" }`
| `BigDecimal`
| converter +
`String`
| `{"value" : "741.99" }`
`NumberDecimal`, `String`
| `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }`
| `URL`
| converter

Loading…
Cancel
Save