Polishing.
Refine documentation. Simplify NumberToNumberConverter. Replace Environment-based configuration with config API. See: #3444 Original pull request: #4916
This commit is contained in:
@@ -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;
|
||||
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 {
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ReadingConverter
|
||||
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
|
||||
@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 {
|
||||
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) {
|
||||
|
||||
@@ -26,15 +26,16 @@ public class Payment {
|
||||
----
|
||||
{
|
||||
"_id" : ObjectId("5ca4a34fa264a01503b36af8"), <1>
|
||||
"value" : NumberDecimal(2.099), <2>
|
||||
"date" : ISODate("2019-04-03T12:11:01.870Z") <3>
|
||||
"value" : NumberDecimal(2.099), <2>
|
||||
"date" : ISODate("2019-04-03T12:11:01.870Z") <3>
|
||||
}
|
||||
----
|
||||
<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 {
|
||||
== 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(…))`.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user