Browse Source

Require explicit BigInteger/BigDecimal conversion settings.

See: #5037
Original Pull Request: #5051
pull/5056/head
Christoph Strobl 4 months ago
parent
commit
8b807890ac
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
  2. 28
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
  3. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java
  4. 126
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  5. 139
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  6. 22
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
  7. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java
  8. 1
      src/main/antora/modules/ROOT/nav.adoc
  9. 60
      src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc
  10. 11
      src/main/antora/modules/ROOT/pages/mongodb/mapping/custom-conversions.adoc

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

@ -132,9 +132,7 @@ abstract class MongoConverters {
List<Object> converters = new ArrayList<>(4); List<Object> converters = new ArrayList<>(4);
converters.add(BigDecimalToStringConverter.INSTANCE); converters.add(BigDecimalToStringConverter.INSTANCE);
converters.add(StringToBigDecimalConverter.INSTANCE);
converters.add(BigIntegerToStringConverter.INSTANCE); converters.add(BigIntegerToStringConverter.INSTANCE);
converters.add(StringToBigIntegerConverter.INSTANCE);
return converters; return converters;
} }
@ -169,6 +167,9 @@ abstract class MongoConverters {
converters.add(ListToVectorConverter.INSTANCE); converters.add(ListToVectorConverter.INSTANCE);
converters.add(BinaryVectorToMongoVectorConverter.INSTANCE); converters.add(BinaryVectorToMongoVectorConverter.INSTANCE);
converters.add(StringToBigDecimalConverter.INSTANCE);
converters.add(StringToBigIntegerConverter.INSTANCE);
converters.add(reading(BsonUndefined.class, Object.class, it -> null)); converters.add(reading(BsonUndefined.class, Object.class, it -> null));
converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString)); converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString));

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

@ -31,8 +31,9 @@ import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterFactory;
@ -63,6 +64,7 @@ import org.springframework.util.Assert;
*/ */
public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions { public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions {
private static final Log LOGGER = LogFactory.getLog(MongoCustomConversions.class);
private static final List<Object> STORE_CONVERTERS; private static final List<Object> STORE_CONVERTERS;
static { static {
@ -153,7 +155,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
LocalDateTime.class); LocalDateTime.class);
private boolean useNativeDriverJavaTimeCodecs = false; private boolean useNativeDriverJavaTimeCodecs = false;
private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128; private BigDecimalRepresentation @Nullable [] bigDecimals;
private final List<Object> customConverters = new ArrayList<>(); private final List<Object> customConverters = new ArrayList<>();
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
@ -310,14 +312,14 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
* Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in * Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in
* MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}. * MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}.
* *
* @param representation the representation to use. * @param representations ordered list of representations to use (first one is default)
* @return this. * @return this.
* @since 4.5 * @since 4.5
*/ */
public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) { public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) {
Assert.notNull(representation, "BigDecimalDataType must not be null"); Assert.notEmpty(representations, "BigDecimalDataType must not be null");
this.bigDecimals = representation; this.bigDecimals = representations;
return this; return this;
} }
@ -372,12 +374,16 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
List<Object> storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); List<Object> storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10);
if (bigDecimals == BigDecimalRepresentation.STRING) { if (bigDecimals != null) {
storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); for (BigDecimalRepresentation representation : bigDecimals) {
switch (representation) {
case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters());
case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters());
} }
}
if (bigDecimals == BigDecimalRepresentation.DECIMAL128) { } else if (LOGGER.isInfoEnabled()) {
storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); LOGGER.info(
"No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format.");
} }
if (useNativeDriverJavaTimeCodecs) { if (useNativeDriverJavaTimeCodecs) {

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java

@ -30,6 +30,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Collation.Alternate; import org.springframework.data.mongodb.core.query.Collation.Alternate;
import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel; import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel;

126
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

@ -15,10 +15,14 @@
*/ */
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.data.mongodb.core.query.Query.*; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.data.mongodb.core.query.Update.*; import static org.assertj.core.api.Assertions.fail;
import static org.springframework.data.mongodb.core.query.Criteria.expr;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import static org.springframework.data.mongodb.core.query.Update.update;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -28,17 +32,29 @@ import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionFailedException;
@ -69,6 +85,7 @@ import org.springframework.data.mongodb.core.aggregation.StringOperators;
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy; import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
@ -144,6 +161,12 @@ public class MongoTemplateTests {
it.defaultDb(DB_NAME); it.defaultDb(DB_NAME);
}); });
cfg.configureConversion(it -> {
it.customConverters(adapter -> {
adapter.bigDecimal(BigDecimalRepresentation.DECIMAL128);
});
});
cfg.configureMappingContext(it -> { cfg.configureMappingContext(it -> {
it.autocreateIndex(false); it.autocreateIndex(false);
it.initialEntitySet(AuditablePerson.class); it.initialEntitySet(AuditablePerson.class);
@ -170,7 +193,10 @@ public class MongoTemplateTests {
}); });
cfg.configureConversion(it -> { cfg.configureConversion(it -> {
it.customConverters(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE); it.customConverters(adapter -> {
adapter.registerConverters(List.of(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE))
.bigDecimal(BigDecimalRepresentation.DECIMAL128);
});
}); });
cfg.configureMappingContext(it -> { cfg.configureMappingContext(it -> {
@ -876,7 +902,7 @@ public class MongoTemplateTests {
} }
@Test // DATAMONGO-602, GH-4920 @Test // DATAMONGO-602, GH-4920
public void testUsingAnInQueryWithBigIntegerId() throws Exception { public void testUsingAnInQueryWithBigIntegerId() {
template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class);
@ -887,6 +913,34 @@ public class MongoTemplateTests {
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1)); assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1));
} }
@Test // GH-5037
public void errorsIfNoBigNumberFormatDefined() {
template = new MongoTestTemplate(cfg -> {
cfg.configureDatabaseFactory(it -> {
it.client(client);
it.defaultDb(DB_NAME);
});
cfg.configureConversion(it -> {
it.customConverters(adapter -> {
// no numeric conversion
});
});
});
template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class);
PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger();
p1.setFirstName("Sven");
p1.setAge(11);
p1.setId(new BigInteger("2666666666666666665"));
assertThatExceptionOfType(CodecConfigurationException.class).isThrownBy(() -> template.insert(p1));
}
@Test @Test
public void testUsingAnInQueryWithPrimitiveIntId() throws Exception { public void testUsingAnInQueryWithPrimitiveIntId() throws Exception {
@ -2561,9 +2615,7 @@ public class MongoTemplateTests {
walter.address = new Address("spring", "data"); walter.address = new Address("spring", "data");
template.save(walter); template.save(walter);
PersonPWA result = template.query(MyPerson.class) PersonPWA result = template.query(MyPerson.class).as(PersonPWA.class).matching(where("id").is(walter.id))
.as(PersonPWA.class)
.matching(where("id").is(walter.id))
.firstValue(); .firstValue();
assertThat(result.getAddress().getCity()).isEqualTo("data"); assertThat(result.getAddress().getCity()).isEqualTo("data");
@ -2571,6 +2623,7 @@ public class MongoTemplateTests {
interface PersonPWA { interface PersonPWA {
String getName(); String getName();
AdressProjection getAddress(); AdressProjection getAddress();
} }
@ -3935,7 +3988,8 @@ public class MongoTemplateTests {
template.save(source); template.save(source);
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
assertThat(raw).containsEntry("field.name.with.dots", "v1"); assertThat(raw).containsEntry("field.name.with.dots", "v1");
} }
@ -3954,13 +4008,17 @@ public class MongoTemplateTests {
template.save(source); template.save(source);
template.save(source2); template.save(source2);
WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname mapping WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))).firstValue(); // mapping
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1")))
.firstValue();
assertThat(loaded).isEqualTo(source); assertThat(loaded).isEqualTo(source);
loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname
.matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))).firstValue(); .matching(
expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1")))
.firstValue();
assertThat(loaded).isEqualTo(source); assertThat(loaded).isEqualTo(source);
} }
@ -3975,20 +4033,20 @@ public class MongoTemplateTests {
template.save(source); template.save(source);
template.update(WithFieldNameContainingDots.class) template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)).apply(AggregationUpdate
.matching(where("id").is(source.id)) .newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))).first();
.apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed"))))
.first();
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
assertThat(raw).containsEntry("field.name.with.dots", "changed"); assertThat(raw).containsEntry("field.name.with.dots", "changed");
template.update(WithFieldNameContainingDots.class) template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id))
.matching(where("id").is(source.id)) .apply(AggregationUpdate.newUpdate(
.apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again"))))
.first(); .first();
raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); raw = template.execute(WithFieldNameContainingDots.class,
collection -> collection.find(new org.bson.Document("_id", source.id)).first());
assertThat(raw).containsEntry("field.name.with.dots", "changed-again"); assertThat(raw).containsEntry("field.name.with.dots", "changed-again");
} }
@ -4013,9 +4071,8 @@ public class MongoTemplateTests {
org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, org.bson.Document raw = template.execute(WithFieldNameContainingDots.class,
collection -> collection.find(new org.bson.Document("_id", source.id)).first()); collection -> collection.find(new org.bson.Document("_id", source.id)).first());
assertThat(raw.get("mapValue", org.bson.Document.class)) assertThat(raw.get("mapValue", org.bson.Document.class)).containsEntry("k1", "v1").containsEntry("map.key.with.dot",
.containsEntry("k1", "v1") "v2");
.containsEntry("map.key.with.dot", "v2");
} }
@Test // GH-4464 @Test // GH-4464
@ -4031,16 +4088,13 @@ public class MongoTemplateTests {
MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter); MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter);
Map<String, String> sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2"); Map<String, String> sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2");
template.execute(WithFieldNameContainingDots.class, template.execute(WithFieldNameContainingDots.class, collection -> {
collection -> {
collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap));
return null; return null;
} });
);
WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class)
.matching(where("id").is("id-1")) .matching(where("id").is("id-1")).firstValue();
.firstValue();
assertThat(loaded.mapValue).isEqualTo(sourceMap); assertThat(loaded.mapValue).isEqualTo(sourceMap);
} }
@ -5015,8 +5069,7 @@ public class MongoTemplateTests {
String id; String id;
@Field(value = "field.name.with.dots", nameType = Type.KEY) @Field(value = "field.name.with.dots", nameType = Type.KEY) String value;
String value;
Map<String, String> mapValue; Map<String, String> mapValue;
@ -5034,7 +5087,8 @@ public class MongoTemplateTests {
return false; return false;
} }
WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o; WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o;
return Objects.equals(id, withFieldNameContainingDots.id) && Objects.equals(value, withFieldNameContainingDots.value) return Objects.equals(id, withFieldNameContainingDots.id)
&& Objects.equals(value, withFieldNameContainingDots.value)
&& Objects.equals(mapValue, withFieldNameContainingDots.mapValue); && Objects.equals(mapValue, withFieldNameContainingDots.mapValue);
} }

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

@ -15,10 +15,22 @@
*/ */
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import static java.time.ZoneId.*; import static java.time.ZoneId.systemDefault;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*; import static org.mockito.Mockito.doReturn;
import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint;
import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
import static org.springframework.data.mongodb.test.util.Assertions.assertThatThrownBy;
import static org.springframework.data.mongodb.test.util.Assertions.fail;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
@ -27,7 +39,24 @@ import java.nio.ByteBuffer;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -53,7 +82,6 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -62,6 +90,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
@ -90,6 +119,8 @@ import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType;
import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.ProjectingType;
import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum; import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
@ -135,8 +166,8 @@ class MappingMongoConverterUnitTests {
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
MongoCustomConversions conversions = new MongoCustomConversions( MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter()
Arrays.asList(new ByteBufferToDoubleHolderConverter())); .registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(BigDecimalRepresentation.DECIMAL128));
mappingContext = new MongoMappingContext(); mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(context); mappingContext.setApplicationContext(context);
@ -396,6 +427,31 @@ class MappingMongoConverterUnitTests {
assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5")); assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5"));
assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class);
} // MappingMongoConverterUnitTests
@Test // DATACMNS-42, DATAMONGO-171, GH-4920
void writesClassWithBigDecimalFails() {
MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter());
mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(context);
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
mappingContext.afterPropertiesSet();
mappingContext.getPersistentEntity(Address.class);
converter = new MappingMongoConverter(resolver, mappingContext);
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(BigDecimal.class);
assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(BigDecimal.class);
} }
@Test // DATACMNS-42, DATAMONGO-171 @Test // DATACMNS-42, DATAMONGO-171
@ -2149,6 +2205,61 @@ class MappingMongoConverterUnitTests {
assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal));
} }
@Test // GH-5037
@SuppressWarnings("deprecation")
void mapsBigIntegerToDecimal128WhenAnnotatedWithFieldTargetTypeWhenDefaultConversionIsSetToString() {
converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128);
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
source.bigDecimal = BigDecimal.valueOf(3.14159D);
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal));
}
@Test // GH-5037
@SuppressWarnings("deprecation")
void mapsBigIntegerToStringWhenNotAnnotatedWithFieldTargetTypeAndDefaultConversionIsSetToString() {
converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128);
BigDecimalContainer source = new BigDecimalContainer();
source.value = BigDecimal.valueOf(3.14159D);
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target.get("value")).isInstanceOf(String.class);
}
@Test // GH-5037
void mapsBigIntegerToStringWhenAnnotatedWithFieldTargetTypeEvenWhenDefaultConverterIsSetToDecimal128() {
converter = createConverter(BigDecimalRepresentation.DECIMAL128);
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
source.bigIntegerAsString = BigInteger.TWO;
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString());
}
@Test // GH-5037
void explicitBigNumberConversionErrorsIfConverterNotRegistered() {
converter = createConverter(BigDecimalRepresentation.STRING);
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
source.bigInteger = BigInteger.TWO;
org.bson.Document target = new org.bson.Document();
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> converter.write(source, target));
}
@Test // DATAMONGO-2328 @Test // DATAMONGO-2328
void mapsDateToLongWhenAnnotatedWithFieldTargetType() { void mapsDateToLongWhenAnnotatedWithFieldTargetType() {
@ -3171,7 +3282,6 @@ class MappingMongoConverterUnitTests {
return nativeValue.getString("bar"); return nativeValue.getString("bar");
} }
@Override @Override
public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) {
return new org.bson.Document("bar", domainValue); return new org.bson.Document("bar", domainValue);
@ -3408,7 +3518,7 @@ class MappingMongoConverterUnitTests {
} }
private MappingMongoConverter createConverter( private MappingMongoConverter createConverter(
MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { MongoCustomConversions.BigDecimalRepresentation... bigDecimalRepresentation) {
MongoCustomConversions conversions = MongoCustomConversions.create( MongoCustomConversions conversions = MongoCustomConversions.create(
it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation));
@ -4081,7 +4191,11 @@ class MappingMongoConverterUnitTests {
@Field(targetType = FieldType.DECIMAL128) // @Field(targetType = FieldType.DECIMAL128) //
BigDecimal bigDecimal; BigDecimal bigDecimal;
@Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; @Field(targetType = FieldType.DECIMAL128) //
BigInteger bigInteger;
@Field(targetType = FieldType.STRING) //
BigInteger bigIntegerAsString;
@Field(targetType = FieldType.INT64) // @Field(targetType = FieldType.INT64) //
Date dateAsLong; Date dateAsLong;
@ -4211,7 +4325,6 @@ class MappingMongoConverterUnitTests {
@WritingConverter @WritingConverter
static class TypeImplementingMapToDocumentConverter implements Converter<TypeImplementingMap, org.bson.Document> { static class TypeImplementingMapToDocumentConverter implements Converter<TypeImplementingMap, org.bson.Document> {
@Override @Override
public org.bson.@Nullable Document convert(TypeImplementingMap source) { public org.bson.@Nullable Document convert(TypeImplementingMap source) {
return new org.bson.Document("1st", source.val1).append("2nd", source.val2); return new org.bson.Document("1st", source.val1).append("2nd", source.val2);
@ -4413,7 +4526,6 @@ class MappingMongoConverterUnitTests {
return value.getString("bar"); return value.getString("bar");
} }
@Override @Override
public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
return new org.bson.Document("bar", value); return new org.bson.Document("bar", value);
@ -4427,7 +4539,6 @@ class MappingMongoConverterUnitTests {
return value.getString("foo"); return value.getString("foo");
} }
@Override @Override
public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) {
return new org.bson.Document("foo", value); return new org.bson.Document("foo", value);

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

@ -55,6 +55,7 @@ import org.springframework.data.mongodb.core.aggregation.EvaluationOperators;
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr; import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DBRef;
@ -97,7 +98,7 @@ public class QueryMapperUnitTests {
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
MongoCustomConversions conversions = new MongoCustomConversions(); MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter().bigDecimal(BigDecimalRepresentation.DECIMAL128));
this.context = new MongoMappingContext(); this.context = new MongoMappingContext();
this.context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); this.context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
@ -152,6 +153,25 @@ public class QueryMapperUnitTests {
assertThat(result).containsEntry("_id", Decimal128.parse("1")); assertThat(result).containsEntry("_id", Decimal128.parse("1"));
} }
@Test // GH-5037
void leavesBigIntegerAsIsIfNotConfigured() {
MongoCustomConversions conversions = new MongoCustomConversions();
context = new MongoMappingContext();
context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context);
converter.setCustomConversions(conversions);
converter.afterPropertiesSet();
mapper = new QueryMapper(converter);
org.bson.Document document = new org.bson.Document("id", new BigInteger("1"));
org.bson.Document result = mapper.getMappedObject(document, context.getPersistentEntity(IdWrapper.class));
assertThat(result).containsEntry("_id", new BigInteger("1"));
}
@Test @Test
void handlesObjectIdCapableBigIntegerIdsCorrectly() { void handlesObjectIdCapableBigIntegerIdsCorrectly() {

6
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java

@ -16,10 +16,12 @@
package org.springframework.data.mongodb.test.util; package org.springframework.data.mongodb.test.util;
import java.util.Arrays; import java.util.Arrays;
import java.util.function.Consumer;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
/** /**
* Utility to configure {@link MongoCustomConversions}. * Utility to configure {@link MongoCustomConversions}.
@ -37,4 +39,8 @@ public class MongoConverterConfigurer {
public void customConverters(Converter<?, ?>... converters) { public void customConverters(Converter<?, ?>... converters) {
customConversions(new MongoCustomConversions(Arrays.asList(converters))); customConversions(new MongoCustomConversions(Arrays.asList(converters)));
} }
public void customConverters(Consumer<MongoConverterConfigurationAdapter> configurer) {
customConversions(MongoCustomConversions.create(configurer));
}
} }

1
src/main/antora/modules/ROOT/nav.adoc

@ -3,6 +3,7 @@
** xref:migration-guides.adoc[] ** xref:migration-guides.adoc[]
*** xref:migration-guide/migration-guide-2.x-to-3.x.adoc[] *** xref:migration-guide/migration-guide-2.x-to-3.x.adoc[]
*** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[]
*** xref:migration-guide/migration-guide-4.x-to-5.x.adoc[]
* xref:mongodb.adoc[] * xref:mongodb.adoc[]
** xref:preface.adoc[] ** xref:preface.adoc[]

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

@ -0,0 +1,60 @@
[[mongodb.migration.4.x-5.x]]
= Migration Guide from 4.x to 5.x
Spring Data MongoDB 4.x requires the MongoDB Java Driver 5.5.x +
To learn more about driver versions please visit the https://www.mongodb.com/docs/drivers/java/sync/current/upgrade/[MongoDB Documentation].
== UUID Representation Changes
Spring Data no longer defaults UUID settings via its configuration support classes, factory beans, nor XML namespace. +
In order to persist UUID values the `UuidRepresentation` hast to be set explicitly.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@Override
protected void configureClientSettings(MongoClientSettings.Builder builder) {
builder.uuidRepresentation(UuidRepresentation.STANDARD);
}
// ...
}
----
XML::
+
[source,xml,indent=0,subs="verbatim,quotes",role="secondary"]
----
<mongo:mongo-client>
<mongo:client-settings uuid-representation="STANDARD"/>
</mongo:mongo-client>
----
======
== BigInteger/BigDecimal Conversion Changes
Spring Data no longer defaults BigInteger/BigDecimal conversion via its configuration support classes.
In order to persist those values the default `BigDecimalRepresentation` hast to be set explicitly.
[source,java]
----
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@Override
protected void configureConverters(MongoConverterConfigurationAdapter configAdapter) {
configAdapter.bigDecimal(BigDecimalRepresentation.DECIMAL128);
}
// ...
}
----
Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default.
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.

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

@ -4,7 +4,7 @@ include::{commons}@data-commons::page$custom-conversions.adoc[]
== Type based Converter == Type based Converter
The most trivial way of influencing the mapping result is by specifying the desired native MongoDB target type via the `@Field` annotation. The most trivial way of influencing the mapping result is by specifying the desired native MongoDB target type via the `@Field` annotation.
This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in native `org.bson.types.Decimal128` format. This allows to work with non MongoDB types like `BigDecimal` in the domain model while persisting values in eg. `String` format.
.Explicit target type mapping .Explicit target type mapping
==== ====
@ -33,8 +33,7 @@ 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] <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. for details.
<2> The desired target type is explicitly defined as `String`. <2> The desired target type is explicitly defined as `String`.
Otherwise, the Otherwise.
`BigDecimal` value would have been turned into a `Decimal128`.
<3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. <3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`.
==== ====
@ -113,8 +112,10 @@ To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted v
This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc. This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc.
With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`. With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`.
As of Spring Data MongoDB 5.0. the default representation of those types moved to MongoDB native `org.bson.types.Decimal128`. As of Spring Data MongoDB 5.0. there no longer is a default representation of those types and conversion needs to be configured explicitly.
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))`. 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))`.
This allows you to make use of the explicit storage type format via `@Field(targetType = DECIMAL128)` while keeping default conversion set to String.
Choosing none of the provided representations is valid as long as those values are no persisted.
[NOTE] [NOTE]
==== ====

Loading…
Cancel
Save