Compare commits

..

35 Commits
3.1.4 ... 3.1.7

Author SHA1 Message Date
Mark Paluch
f52cc3be1f Release version 3.1.7 (2020.0.7).
See #3598
2021-03-31 18:19:52 +02:00
Mark Paluch
1bda93858c Prepare 3.1.7 (2020.0.7).
See #3598
2021-03-31 18:19:19 +02:00
Mark Paluch
1808970daf Updated changelog.
See #3598
2021-03-31 18:19:13 +02:00
Mark Paluch
558fc28cce Updated changelog.
See #3595
2021-03-31 17:26:09 +02:00
Mark Paluch
16bef54f11 Use StringUtils.replace(…) instead of String.replaceAll(…) for mapKeyDotReplacement.
We now use StringUtils.replace(…) to replace the map key dot in MappingMongoConverter. StringUtils perform a plain search instead of using Regex which improves the overall performance.

Closes #3613
2021-03-30 14:29:50 +02:00
Mark Paluch
d68a812e1b Polishing.
Omit StreamUtils usage if input is a collection. Remove superfluous Flux.from(…). Simplify test and migrate test to JUnit 5.

See #3609.
Original pull request: #3611.
2021-03-29 11:02:34 +02:00
Clément Petit
ccb9f111d9 Return saved entity reference instead of original reference.
Make SimpleReactiveMongoRepository#saveAll(Publisher<S>) return the saved entity references instead of the original references.

Closes #3609
Original pull request: #3611.
2021-03-29 10:55:36 +02:00
Mark Paluch
f64b177c8f Updated changelog.
See #3558
2021-03-17 11:31:32 +01:00
Mark Paluch
c0c7ba767f After release cleanups.
See #3561
2021-03-17 11:02:15 +01:00
Mark Paluch
7639701f3f Prepare next development iteration.
See #3561
2021-03-17 11:02:13 +01:00
Mark Paluch
b39b2591b6 Release version 3.1.6 (2020.0.6).
See #3561
2021-03-17 10:54:12 +01:00
Mark Paluch
65c8317e38 Prepare 3.1.6 (2020.0.6).
See #3561
2021-03-17 10:53:43 +01:00
Mark Paluch
9d0f7bac6a Updated changelog.
See #3561
2021-03-17 10:53:41 +01:00
Mark Paluch
6f50747d21 Updated changelog.
See #3556
2021-03-17 10:35:15 +01:00
Mark Paluch
5cf1578ad3 Polishing.
Move hasValue(…) from DocumentAccessor to BsonUtils. Fix typo in tests.

See: #3590
Original pull request: #3591.
2021-03-15 14:03:33 +01:00
Christoph Strobl
78a59c45ca Fix ShardKey lookup for nested paths.
This commit fixes the lookup of shard key values for nested paths using the dot (.) notation.

Closes: #3590
Original pull request: #3591.
2021-03-15 14:02:49 +01:00
Christoph Strobl
dccdfc8b4d Upgrade MongoDB drivers to 4.1.2
Closes #3589
2021-03-15 09:17:52 +01:00
Mark Paluch
e48239eb8f Remove @Persistent from entity-scan include filters.
We now only scan for entities annotated with `@Document` to avoid inclusion of non-MongoDB entities. Previously, types annotated (or meta-annotated) with `@Persistent` were included as MongoDB entity which could lead to mapping rule violations.

Closes #3592
2021-03-11 15:08:05 +01:00
Christoph Strobl
c3b4f61d29 Preserve class keyword as Map key during update mapping.
This commit makes sure to skip the class property ob Object when mapping maps and their keys inside an Update.

Closes #3566
Original pull request: #3577.
2021-03-02 11:38:20 +01:00
Mark Paluch
22ed860b4a Polishing.
Reformat code. Reduce method visibility in JUnit 5 tests. Add Nullable annotations to address warnings.

See #3568
Original pull request: #3569.
2021-03-02 11:30:26 +01:00
Brice Vandeputte
bf642ad3f7 Translate MongoSocketException subclasses to DataAccessResourceFailureException.
Closes #3568
Original pull request: #3569.
2021-03-02 11:30:26 +01:00
Christoph Strobl
fcd48539ea Remove duplicate JSON Schema section from reference documentation.
Closes: #3573
Original pull request: #3574.
2021-03-01 14:42:33 +01:00
Mark Paluch
bf10f72a57 Polishing.
Simplify assertions.

See #3552.
Original pull request: #3565.
2021-02-22 09:57:03 +01:00
Christoph Strobl
1c652cce1c Preserve numeric keys that exceed Long range when mapping Updates.
This commit makes sure we preserve map keys no matter what.

Closes #3552.
Original pull request: #3565.
2021-02-22 09:57:02 +01:00
Mark Paluch
dc2de878bc Polishing.
Reformat code. Add since tags.

See #3395
Original pull request: #3554.
2021-02-18 15:08:49 +01:00
Christoph Strobl
00cacc02ac Fix case insensitive derived in queries on String properties.
We now consider the IgnoreCase part of a derived query when used along with In. Strings will be quoted to avoid malicious strings from being handed over to the server as a regular expression to evaluate.

See #3395
Original pull request: #3554.
2021-02-18 15:08:49 +01:00
Christoph Strobl
811c2e5d7b Updated changelog.
See #3560
2021-02-18 11:37:47 +01:00
Christoph Strobl
200f3006bd After release cleanups.
See #3557
2021-02-18 11:12:46 +01:00
Christoph Strobl
1d6bea51ec Prepare next development iteration.
See #3557
2021-02-18 11:12:44 +01:00
Christoph Strobl
7779ded45c Release version 3.1.5 (2020.0.5).
See #3557
2021-02-18 10:59:16 +01:00
Christoph Strobl
918bf7c138 Prepare 3.1.5 (2020.0.5).
See #3557
2021-02-18 10:58:49 +01:00
Christoph Strobl
abe3b9f6d7 Updated changelog.
See #3557
2021-02-18 10:58:42 +01:00
Christoph Strobl
41c453cc83 Updated changelog.
See #3537
2021-02-17 14:20:39 +01:00
Christoph Strobl
77784d88c7 After release cleanups.
See #3536
2021-02-17 13:41:54 +01:00
Christoph Strobl
263c62c880 Prepare next development iteration.
See #3536
2021-02-17 13:41:53 +01:00
24 changed files with 495 additions and 388 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.1.4</version>
<version>3.1.7</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>2.4.4</version>
<version>2.4.7</version>
</parent>
<modules>
@@ -26,8 +26,8 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>2.4.4</springdata.commons>
<mongo>4.1.1</mongo>
<springdata.commons>2.4.7</springdata.commons>
<mongo>4.1.2</mongo>
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>
</properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.1.4</version>
<version>3.1.7</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.1.4</version>
<version>3.1.7</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.1.4</version>
<version>3.1.7</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -26,7 +26,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
import org.springframework.data.mapping.model.FieldNamingStrategy;
@@ -140,8 +139,7 @@ public abstract class MongoConfigurationSupport {
}
/**
* Scans the given base package for entities, i.e. MongoDB specific types annotated with {@link Document} and
* {@link Persistent}.
* Scans the given base package for entities, i.e. MongoDB specific types annotated with {@link Document}.
*
* @param basePackage must not be {@literal null}.
* @return
@@ -161,7 +159,6 @@ public abstract class MongoConfigurationSupport {
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {

View File

@@ -21,6 +21,7 @@ import java.util.HashSet;
import java.util.Set;
import org.bson.BsonInvalidOperationException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
@@ -39,6 +40,7 @@ import org.springframework.util.ClassUtils;
import com.mongodb.MongoBulkWriteException;
import com.mongodb.MongoException;
import com.mongodb.MongoServerException;
import com.mongodb.MongoSocketException;
import com.mongodb.bulk.BulkWriteError;
/**
@@ -49,6 +51,7 @@ import com.mongodb.bulk.BulkWriteError;
* @author Oliver Gierke
* @author Michal Vich
* @author Christoph Strobl
* @author Brice Vandeputte
*/
public class MongoExceptionTranslator implements PersistenceExceptionTranslator {
@@ -78,6 +81,10 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
throw new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
}
if (ex instanceof MongoSocketException) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
}
String exception = ClassUtils.getShortName(ClassUtils.getUserClass(ex.getClass()));
if (DUPLICATE_KEY_EXCEPTIONS.contains(exception)) {

View File

@@ -658,7 +658,8 @@ class QueryOperations {
: mappedDocument != null ? mappedDocument.getDocument() : getMappedUpdate(domainType);
Document filterWithShardKey = new Document(filter);
getMappedShardKeyFields(domainType).forEach(key -> filterWithShardKey.putIfAbsent(key, shardKeySource.get(key)));
getMappedShardKeyFields(domainType)
.forEach(key -> filterWithShardKey.putIfAbsent(key, BsonUtils.resolveValue(shardKeySource, key)));
return filterWithShardKey;
}

View File

@@ -17,17 +17,16 @@ package org.springframework.data.mongodb.core.convert;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
@@ -110,28 +109,7 @@ class DocumentAccessor {
*/
@Nullable
public Object get(MongoPersistentProperty property) {
String fieldName = property.getFieldName();
Map<String, Object> map = BsonUtils.asMap(document);
if (!fieldName.contains(".")) {
return map.get(fieldName);
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = map;
Object result = null;
while (source != null && parts.hasNext()) {
result = source.get(parts.next());
if (parts.hasNext()) {
source = getAsMap(result);
}
}
return result;
return BsonUtils.resolveValue(document, property.getFieldName());
}
/**
@@ -157,71 +135,7 @@ class DocumentAccessor {
Assert.notNull(property, "Property must not be null!");
String fieldName = property.getFieldName();
if (this.document instanceof Document) {
if (((Document) this.document).containsKey(fieldName)) {
return true;
}
} else if (this.document instanceof DBObject) {
if (((DBObject) this.document).containsField(fieldName)) {
return true;
}
}
if (!fieldName.contains(".")) {
return false;
}
String[] parts = fieldName.split("\\.");
Map<String, Object> source;
if (this.document instanceof Document) {
source = ((Document) this.document);
} else {
source = ((DBObject) this.document).toMap();
}
Object result = null;
for (int i = 1; i < parts.length; i++) {
result = source.get(parts[i - 1]);
source = getAsMap(result);
if (source == null) {
return false;
}
}
return source.containsKey(parts[parts.length - 1]);
}
/**
* Returns the given source object as map, i.e. {@link Document}s and maps as is or {@literal null} otherwise.
*
* @param source can be {@literal null}.
* @return can be {@literal null}.
*/
@Nullable
@SuppressWarnings("unchecked")
private static Map<String, Object> getAsMap(Object source) {
if (source instanceof Document) {
return (Document) source;
}
if (source instanceof BasicDBObject) {
return (BasicDBObject) source;
}
if (source instanceof Map) {
return (Map<String, Object>) source;
}
return null;
return BsonUtils.hasValue(document, property.getFieldName());
}
/**

View File

@@ -75,6 +75,7 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
@@ -182,6 +183,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire
* object to fail. If further customization of the translation is needed, have a look at
* {@link #potentiallyEscapeMapKey(String)} as well as {@link #potentiallyUnescapeMapKey(String)}.
* <p>
* {@code mapKeyDotReplacement} is used as-is during replacement operations without further processing (i.e. regex or
* normalization).
*
* @param mapKeyDotReplacement the mapKeyDotReplacement to set. Can be {@literal null}.
*/
@@ -900,7 +904,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
source));
}
return source.replaceAll("\\.", mapKeyDotReplacement);
return StringUtils.replace(source, ".", mapKeyDotReplacement);
}
/**
@@ -928,7 +932,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @return
*/
protected String potentiallyUnescapeMapKey(String source) {
return mapKeyDotReplacement == null ? source : source.replaceAll(mapKeyDotReplacement, "\\.");
return mapKeyDotReplacement == null ? source : StringUtils.replace(source, mapKeyDotReplacement, ".");
}
/**

View File

@@ -1152,7 +1152,8 @@ public class QueryMapper {
private boolean isPathToJavaLangClassProperty(PropertyPath path) {
if (path.getType().equals(Class.class) && path.getLeafProperty().getOwningType().getType().equals(Class.class)) {
if ((path.getType() == Class.class || path.getType().equals(Object.class))
&& path.getLeafProperty().getType() == Class.class) {
return true;
}
return false;
@@ -1261,9 +1262,9 @@ public class QueryMapper {
String partial = iterator.next();
boolean isPositional = (isPositionalParameter(partial) && (property.isMap() || property.isCollectionLike()));
boolean isPositional = isPositionalParameter(partial) && property.isCollectionLike();
if (isPositional) {
if (isPositional || property.isMap()) {
mappedName.append(".").append(partial);
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.query;
import java.util.regex.Pattern;
import org.bson.BsonRegularExpression;
import org.springframework.lang.Nullable;
/**
@@ -102,6 +103,15 @@ public enum MongoRegexCreator {
}
}
/**
* @param source
* @return
* @since 2.2.14
*/
public Object toCaseInsensitiveMatch(Object source) {
return source instanceof String ? new BsonRegularExpression(Pattern.quote((String) source), "i") : source;
}
private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, MatchMode matcherType) {
if (MatchMode.REGEX == matcherType) {

View File

@@ -25,7 +25,6 @@ import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.domain.Sort;
@@ -51,8 +50,10 @@ import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Custom query creator to create Mongo criterias.
@@ -196,9 +197,9 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case IS_NULL:
return criteria.is(null);
case NOT_IN:
return criteria.nin(nextAsArray(parameters));
return criteria.nin(nextAsList(parameters, part));
case IN:
return criteria.in(nextAsArray(parameters));
return criteria.in(nextAsList(parameters, part));
case LIKE:
case STARTING_WITH:
case ENDING_WITH:
@@ -337,7 +338,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
Iterator<Object> parameters) {
if (property.isCollectionLike()) {
return criteria.in(nextAsArray(parameters));
return criteria.in(nextAsList(parameters, part));
}
return addAppropriateLikeRegexTo(criteria, part, parameters.next());
@@ -400,17 +401,24 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
String.format("Expected parameter type of %s but got %s!", type, parameter.getClass()));
}
private Object[] nextAsArray(Iterator<Object> iterator) {
private java.util.List<?> nextAsList(Iterator<Object> iterator, Part part) {
Object next = iterator.next();
if (next instanceof Collection) {
return ((Collection<?>) next).toArray();
} else if (next != null && next.getClass().isArray()) {
return (Object[]) next;
Streamable<?> streamable = asStreamable(iterator.next());
if (!isSimpleComparisionPossible(part)) {
streamable = streamable.map(MongoRegexCreator.INSTANCE::toCaseInsensitiveMatch);
}
return new Object[] { next };
return streamable.toList();
}
private Streamable<?> asStreamable(Object value) {
if (value instanceof Collection) {
return Streamable.of((Collection<?>) value);
} else if (ObjectUtils.isArray(value)) {
return Streamable.of((Object[]) value);
}
return Streamable.of(value);
}
private String toLikeRegex(String source, Part part) {

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.support;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -215,7 +216,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Assert.notNull(ids, "The given Ids of entities not be null!");
return findAll(new Query(new Criteria(entityInformation.getIdAttribute())
.in(Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList()))));
.in(toCollection(ids))));
}
/*
@@ -266,10 +267,10 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Assert.notNull(entities, "The given Iterable of entities not be null!");
List<S> list = Streamable.of(entities).stream().collect(StreamUtils.toUnmodifiableList());
Collection<S> list = toCollection(entities);
if (list.isEmpty()) {
return list;
return Collections.emptyList();
}
return new ArrayList<>(mongoOperations.insertAll(list));
@@ -374,6 +375,11 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
return where(entityInformation.getIdAttribute()).is(id);
}
private static <E> Collection<E> toCollection(Iterable<E> ids) {
return ids instanceof Collection ? (Collection<E>) ids
: StreamUtils.createStreamFromIterator(ids.iterator()).collect(Collectors.toList());
}
private List<T> findAll(@Nullable Query query) {
if (query == null) {

View File

@@ -21,10 +21,11 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.Serializable;
import java.util.List;
import java.util.Collection;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Example;
@@ -47,6 +48,7 @@ import com.mongodb.client.result.DeleteResult;
* @author Oliver Gierke
* @author Christoph Strobl
* @author Ruben J Garcia
* @author Clément Petit
* @since 2.0
*/
public class SimpleReactiveMongoRepository<T, ID extends Serializable> implements ReactiveMongoRepository<T, ID> {
@@ -173,7 +175,7 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Assert.notNull(ids, "The given Iterable of Id's must not be null!");
return findAll(new Query(new Criteria(entityInformation.getIdAttribute())
.in(Streamable.of(ids).stream().collect(StreamUtils.toUnmodifiableList()))));
.in(toCollection(ids))));
}
/*
@@ -274,9 +276,9 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Assert.notNull(entities, "The given Iterable of entities must not be null!");
List<S> source = Streamable.of(entities).stream().collect(StreamUtils.toUnmodifiableList());
Collection<S> source = toCollection(entities);
return source.isEmpty() ? Flux.empty() : Flux.from(mongoOperations.insertAll(source));
return source.isEmpty() ? Flux.empty() : mongoOperations.insertAll(source);
}
/*
@@ -333,8 +335,8 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Assert.notNull(entityStream, "The given Publisher of entities must not be null!");
return Flux.from(entityStream).flatMap(entity -> entityInformation.isNew(entity) ? //
mongoOperations.insert(entity, entityInformation.getCollectionName()).then(Mono.just(entity)) : //
mongoOperations.save(entity, entityInformation.getCollectionName()).then(Mono.just(entity)));
mongoOperations.insert(entity, entityInformation.getCollectionName()) : //
mongoOperations.save(entity, entityInformation.getCollectionName()));
}
/*
@@ -436,8 +438,12 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
return where(entityInformation.getIdAttribute()).is(id);
}
private Flux<T> findAll(Query query) {
private static <E> Collection<E> toCollection(Iterable<E> ids) {
return ids instanceof Collection ? (Collection<E>) ids
: StreamUtils.createStreamFromIterator(ids.iterator()).collect(Collectors.toList());
}
private Flux<T> findAll(Query query) {
return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName());
}
}

View File

@@ -282,6 +282,109 @@ public class BsonUtils {
.orElseGet(() -> new DocumentCodec(codecRegistryProvider.getCodecRegistry())));
}
/**
* Resolve a the value for a given key. If the given {@link Bson} value contains the key the value is immediately
* returned. If not and the key contains a path using the dot ({@code .}) notation it will try to resolve the path by
* inspecting the individual parts. If one of the intermediate ones is {@literal null} or cannot be inspected further
* (wrong) type, {@literal null} is returned.
*
* @param bson the source to inspect. Must not be {@literal null}.
* @param key the key to lookup. Must not be {@literal null}.
* @return can be {@literal null}.
* @since 3.0.8
*/
@Nullable
public static Object resolveValue(Bson bson, String key) {
Map<String, Object> source = asMap(bson);
if (source.containsKey(key) || !key.contains(".")) {
return source.get(key);
}
String[] parts = key.split("\\.");
for (int i = 1; i < parts.length; i++) {
Object result = source.get(parts[i - 1]);
if (!(result instanceof Bson)) {
return null;
}
source = asMap((Bson) result);
}
return source.get(parts[parts.length - 1]);
}
/**
* Returns whether the underlying {@link Bson bson} has a value ({@literal null} or non-{@literal null}) for the given
* {@code key}.
*
* @param bson the source to inspect. Must not be {@literal null}.
* @param key the key to lookup. Must not be {@literal null}.
* @return {@literal true} if no non {@literal null} value present.
* @since 3.0.8
*/
public static boolean hasValue(Bson bson, String key) {
Map<String, Object> source = asMap(bson);
if (source.get(key) != null) {
return true;
}
if (!key.contains(".")) {
return false;
}
String[] parts = key.split("\\.");
Object result;
for (int i = 1; i < parts.length; i++) {
result = source.get(parts[i - 1]);
source = getAsMap(result);
if (source == null) {
return false;
}
}
return source.containsKey(parts[parts.length - 1]);
}
/**
* Returns the given source object as map, i.e. {@link Document}s and maps as is or {@literal null} otherwise.
*
* @param source can be {@literal null}.
* @return can be {@literal null}.
*/
@Nullable
@SuppressWarnings("unchecked")
private static Map<String, Object> getAsMap(Object source) {
if (source instanceof Document) {
return (Document) source;
}
if (source instanceof BasicDBObject) {
return (BasicDBObject) source;
}
if (source instanceof DBObject) {
return ((DBObject) source).toMap();
}
if (source instanceof Map) {
return (Map<String, Object>) source;
}
return null;
}
@Nullable
private static String toJson(@Nullable Object value) {

View File

@@ -17,8 +17,6 @@ package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import java.net.UnknownHostException;
import org.bson.BsonDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -32,11 +30,14 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.data.mongodb.ClientSessionException;
import org.springframework.data.mongodb.MongoTransactionException;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.lang.Nullable;
import com.mongodb.MongoCursorNotFoundException;
import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketReadTimeoutException;
import com.mongodb.MongoSocketWriteException;
import com.mongodb.ServerAddress;
/**
@@ -45,18 +46,20 @@ import com.mongodb.ServerAddress;
* @author Michal Vich
* @author Oliver Gierke
* @author Christoph Strobl
* @author Brice Vandeputte
*/
public class MongoExceptionTranslatorUnitTests {
class MongoExceptionTranslatorUnitTests {
MongoExceptionTranslator translator;
private static final String EXCEPTION_MESSAGE = "IOException";
private MongoExceptionTranslator translator;
@BeforeEach
public void setUp() {
void setUp() {
translator = new MongoExceptionTranslator();
}
@Test
public void translateDuplicateKey() {
void translateDuplicateKey() {
expectExceptionWithCauseMessage(
translator.translateExceptionIfPossible(
@@ -64,17 +67,33 @@ public class MongoExceptionTranslatorUnitTests {
DuplicateKeyException.class, null);
}
@Test
public void translateSocketException() {
@Test // GH-3568
void translateSocketException() {
expectExceptionWithCauseMessage(
translator.translateExceptionIfPossible(new MongoSocketException("IOException", new ServerAddress())),
DataAccessResourceFailureException.class, "IOException");
translator.translateExceptionIfPossible(new MongoSocketException(EXCEPTION_MESSAGE, new ServerAddress())),
DataAccessResourceFailureException.class, EXCEPTION_MESSAGE);
}
@Test // GH-3568
void translateSocketExceptionSubclasses() {
expectExceptionWithCauseMessage(
translator.translateExceptionIfPossible(
new MongoSocketWriteException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE))
),
DataAccessResourceFailureException.class, EXCEPTION_MESSAGE);
expectExceptionWithCauseMessage(
translator.translateExceptionIfPossible(
new MongoSocketReadTimeoutException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE))
),
DataAccessResourceFailureException.class, EXCEPTION_MESSAGE);
}
@Test
public void translateCursorNotFound() throws UnknownHostException {
void translateCursorNotFound() {
expectExceptionWithCauseMessage(
translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())),
@@ -82,21 +101,21 @@ public class MongoExceptionTranslatorUnitTests {
}
@Test
public void translateToDuplicateKeyException() {
void translateToDuplicateKeyException() {
checkTranslatedMongoException(DuplicateKeyException.class, 11000);
checkTranslatedMongoException(DuplicateKeyException.class, 11001);
}
@Test
public void translateToDataAccessResourceFailureException() {
void translateToDataAccessResourceFailureException() {
checkTranslatedMongoException(DataAccessResourceFailureException.class, 12000);
checkTranslatedMongoException(DataAccessResourceFailureException.class, 13440);
}
@Test
public void translateToInvalidDataAccessApiUsageException() {
void translateToInvalidDataAccessApiUsageException() {
checkTranslatedMongoException(InvalidDataAccessApiUsageException.class, 10003);
checkTranslatedMongoException(InvalidDataAccessApiUsageException.class, 12001);
@@ -106,7 +125,7 @@ public class MongoExceptionTranslatorUnitTests {
}
@Test
public void translateToUncategorizedMongoDbException() {
void translateToUncategorizedMongoDbException() {
MongoException exception = new MongoException(0, "");
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
@@ -115,7 +134,7 @@ public class MongoExceptionTranslatorUnitTests {
}
@Test
public void translateMongoInternalException() {
void translateMongoInternalException() {
MongoInternalException exception = new MongoInternalException("Internal exception");
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
@@ -124,14 +143,14 @@ public class MongoExceptionTranslatorUnitTests {
}
@Test
public void translateUnsupportedException() {
void translateUnsupportedException() {
RuntimeException exception = new RuntimeException();
assertThat(translator.translateExceptionIfPossible(exception)).isNull();
}
@Test // DATAMONGO-2045
public void translateSessionExceptions() {
void translateSessionExceptions() {
checkTranslatedMongoException(ClientSessionException.class, 206);
checkTranslatedMongoException(ClientSessionException.class, 213);
@@ -140,7 +159,7 @@ public class MongoExceptionTranslatorUnitTests {
}
@Test // DATAMONGO-2045
public void translateTransactionExceptions() {
void translateTransactionExceptions() {
checkTranslatedMongoException(MongoTransactionException.class, 217);
checkTranslatedMongoException(MongoTransactionException.class, 225);
@@ -163,13 +182,13 @@ public class MongoExceptionTranslatorUnitTests {
assertThat(((MongoException) cause).getCode()).isEqualTo(code);
}
private static void expectExceptionWithCauseMessage(NestedRuntimeException e,
private static void expectExceptionWithCauseMessage(@Nullable NestedRuntimeException e,
Class<? extends NestedRuntimeException> type) {
expectExceptionWithCauseMessage(e, type, null);
}
private static void expectExceptionWithCauseMessage(NestedRuntimeException e,
Class<? extends NestedRuntimeException> type, String message) {
private static void expectExceptionWithCauseMessage(@Nullable NestedRuntimeException e,
Class<? extends NestedRuntimeException> type, @Nullable String message) {
assertThat(e).isInstanceOf(type);

View File

@@ -84,6 +84,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.Sharded;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
@@ -1922,6 +1923,24 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(findIterable, never()).first();
}
@Test // GH-3590
void shouldIncludeValueFromNestedShardKeyPath() {
WithShardKeyPointingToNested source = new WithShardKeyPointingToNested();
source.id = "id-1";
source.value = "v1";
source.nested = new WithNamedFields();
source.nested.customName = "cname";
source.nested.name = "name";
template.save(source);
ArgumentCaptor<Bson> filter = ArgumentCaptor.forClass(Bson.class);
verify(collection).replaceOne(filter.capture(), any(), any());
assertThat(filter.getValue()).isEqualTo(new Document("_id", "id-1").append("value", "v1").append("nested.custom-named-field", "cname"));
}
@Test // DATAMONGO-2341
void saveShouldProjectOnShardKeyWhenLoadingExistingDocument() {
@@ -2267,6 +2286,13 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Field("firstname") String name;
}
@Sharded(shardKey = {"value", "nested.customName"})
static class WithShardKeyPointingToNested {
String id;
String value;
WithNamedFields nested;
}
/**
* Mocks out the {@link MongoTemplate#getDb()} method to return the {@link DB} mock instead of executing the actual
* behaviour.

View File

@@ -1089,6 +1089,38 @@ class UpdateMapperUnitTests {
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element].value", 10)));
}
@Test // GH-3552
void numericKeyForMap() {
Update update = new Update().set("map.601218778970110001827396", "testing");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithObjectMap.class));
assertThat(mappedUpdate).isEqualTo("{\"$set\": {\"map.601218778970110001827396\": \"testing\"}}");
}
@Test // GH-3552
void numericKeyInMapOfNestedPath() {
Update update = new Update().set("map.601218778970110001827396.value", "testing");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithObjectMap.class));
assertThat(mappedUpdate)
.isEqualTo("{\"$set\": {\"map.601218778970110001827396.value\": \"testing\"}}");
}
@Test // GH-3566
void mapsObjectClassPropertyFieldInMapValueTypeAsKey() {
Update update = new Update().set("map.class", "value");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithObjectMap.class));
assertThat(mappedUpdate)
.isEqualTo("{\"$set\": {\"map.class\": \"value\"}}");
}
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}

View File

@@ -1363,4 +1363,19 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Dave")).containsExactly(dave);
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter")).containsExactly(carter);
}
@Test // GH-3395
void caseInSensitiveInClause() {
assertThat(repository.findByLastnameIgnoreCaseIn("bEAuFoRd", "maTTheWs")).hasSize(3);
}
@Test // GH-3395
void caseInSensitiveInClauseQuotesExpressions() {
assertThat(repository.findByLastnameIgnoreCaseIn(".*")).isEmpty();
}
@Test // GH-3395
void caseSensitiveInClauseIgnoresExpressions() {
assertThat(repository.findByFirstnameIn(".*")).isEmpty();
}
}

View File

@@ -125,6 +125,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
List<Person> findByLastnameIgnoreCaseIn(String... lastname);
/**
* Returns all {@link Person}s with a firstname contained in the given varargs.
*

View File

@@ -20,15 +20,19 @@ import static org.springframework.data.domain.ExampleMatcher.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.With;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -44,10 +48,9 @@ import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory;
import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.util.ClassUtils;
/**
@@ -56,19 +59,22 @@ import org.springframework.util.ClassUtils;
* @author Mark Paluch
* @author Christoph Strobl
* @author Ruben J Garcia
* @author Clément Petit
*/
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:reactive-infrastructure.xml")
public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanFactoryAware {
@Autowired private ReactiveMongoTemplate template;
ReactiveMongoRepositoryFactory factory;
ClassLoader classLoader;
BeanFactory beanFactory;
ReactivePersonRepostitory repository;
private ReactiveMongoRepositoryFactory factory;
private ClassLoader classLoader;
private BeanFactory beanFactory;
private ReactivePersonRepository repository;
private ReactiveImmutablePersonRepository immutableRepository;
private ReactivePerson dave, oliver, carter, boyd, stefan, leroi, alicia;
private ImmutableReactivePerson keith, james, mariah;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
@@ -80,8 +86,8 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
this.beanFactory = beanFactory;
}
@Before
public void setUp() {
@BeforeEach
void setUp() {
factory = new ReactiveMongoRepositoryFactory(template);
factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class);
@@ -89,9 +95,11 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
factory.setBeanFactory(beanFactory);
factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT);
repository = factory.getRepository(ReactivePersonRepostitory.class);
repository = factory.getRepository(ReactivePersonRepository.class);
immutableRepository = factory.getRepository(ReactiveImmutablePersonRepository.class);
repository.deleteAll().as(StepVerifier::create).verifyComplete();
immutableRepository.deleteAll().as(StepVerifier::create).verifyComplete();
dave = new ReactivePerson("Dave", "Matthews", 42);
oliver = new ReactivePerson("Oliver August", "Matthews", 4);
@@ -100,6 +108,9 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
stefan = new ReactivePerson("Stefan", "Lessard", 34);
leroi = new ReactivePerson("Leroi", "Moore", 41);
alicia = new ReactivePerson("Alicia", "Keys", 30);
keith = new ImmutableReactivePerson(null, "Keith", "Urban", 53);
james = new ImmutableReactivePerson(null, "James", "Arthur", 33);
mariah = new ImmutableReactivePerson(null, "Mariah", "Carey", 51);
repository.saveAll(Arrays.asList(oliver, dave, carter, boyd, stefan, leroi, alicia)).as(StepVerifier::create) //
.expectNextCount(7) //
@@ -107,78 +118,78 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void existsByIdShouldReturnTrueForExistingObject() {
void existsByIdShouldReturnTrueForExistingObject() {
repository.existsById(dave.id).as(StepVerifier::create).expectNext(true).verifyComplete();
}
@Test // DATAMONGO-1444
public void existsByIdShouldReturnFalseForAbsentObject() {
void existsByIdShouldReturnFalseForAbsentObject() {
repository.existsById("unknown").as(StepVerifier::create).expectNext(false).verifyComplete();
}
@Test // DATAMONGO-1444
public void existsByMonoOfIdShouldReturnTrueForExistingObject() {
void existsByMonoOfIdShouldReturnTrueForExistingObject() {
repository.existsById(Mono.just(dave.id)).as(StepVerifier::create).expectNext(true).verifyComplete();
}
@Test // DATAMONGO-1712
public void existsByFluxOfIdShouldReturnTrueForExistingObject() {
void existsByFluxOfIdShouldReturnTrueForExistingObject() {
repository.existsById(Flux.just(dave.id, oliver.id)).as(StepVerifier::create).expectNext(true).verifyComplete();
}
@Test // DATAMONGO-1444
public void existsByEmptyMonoOfIdShouldReturnEmptyMono() {
void existsByEmptyMonoOfIdShouldReturnEmptyMono() {
repository.existsById(Mono.empty()).as(StepVerifier::create).verifyComplete();
}
@Test // DATAMONGO-1444
public void findByIdShouldReturnObject() {
void findByIdShouldReturnObject() {
repository.findById(dave.id).as(StepVerifier::create).expectNext(dave).verifyComplete();
}
@Test // DATAMONGO-1444
public void findByIdShouldCompleteWithoutValueForAbsentObject() {
void findByIdShouldCompleteWithoutValueForAbsentObject() {
repository.findById("unknown").as(StepVerifier::create).verifyComplete();
}
@Test // DATAMONGO-1444
public void findByIdByMonoOfIdShouldReturnTrueForExistingObject() {
void findByIdByMonoOfIdShouldReturnTrueForExistingObject() {
repository.findById(Mono.just(dave.id)).as(StepVerifier::create).expectNext(dave).verifyComplete();
}
@Test // DATAMONGO-1712
public void findByIdByFluxOfIdShouldReturnTrueForExistingObject() {
void findByIdByFluxOfIdShouldReturnTrueForExistingObject() {
repository.findById(Flux.just(dave.id, oliver.id)).as(StepVerifier::create).expectNext(dave).verifyComplete();
}
@Test // DATAMONGO-1444
public void findByIdByEmptyMonoOfIdShouldReturnEmptyMono() {
void findByIdByEmptyMonoOfIdShouldReturnEmptyMono() {
repository.findById(Mono.empty()).as(StepVerifier::create).verifyComplete();
}
@Test // DATAMONGO-1444
public void findAllShouldReturnAllResults() {
void findAllShouldReturnAllResults() {
repository.findAll().as(StepVerifier::create).expectNextCount(7).verifyComplete();
}
@Test // DATAMONGO-1444
public void findAllByIterableOfIdShouldReturnResults() {
void findAllByIterableOfIdShouldReturnResults() {
repository.findAllById(Arrays.asList(dave.id, boyd.id)).as(StepVerifier::create).expectNextCount(2)
.verifyComplete();
}
@Test // DATAMONGO-1444
public void findAllByPublisherOfIdShouldReturnResults() {
void findAllByPublisherOfIdShouldReturnResults() {
repository.findAllById(Flux.just(dave.id, boyd.id)).as(StepVerifier::create).expectNextCount(2).verifyComplete();
}
@Test // DATAMONGO-1444
public void findAllByEmptyPublisherOfIdShouldReturnResults() {
void findAllByEmptyPublisherOfIdShouldReturnResults() {
repository.findAllById(Flux.empty()).as(StepVerifier::create).verifyComplete();
}
@Test // DATAMONGO-1444
public void findAllWithSortShouldReturnResults() {
void findAllWithSortShouldReturnResults() {
repository.findAll(Sort.by(new Order(Direction.ASC, "age"))).as(StepVerifier::create) //
.expectNextCount(7) //
@@ -186,12 +197,12 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void countShouldReturnNumberOfRecords() {
void countShouldReturnNumberOfRecords() {
repository.count().as(StepVerifier::create).expectNext(7L).verifyComplete();
}
@Test // DATAMONGO-1444
public void insertEntityShouldInsertEntity() {
void insertEntityShouldInsertEntity() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -203,7 +214,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void insertShouldDeferredWrite() {
void insertShouldDeferredWrite() {
ReactivePerson person = new ReactivePerson("Homer", "Simpson", 36);
@@ -213,7 +224,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void insertIterableOfEntitiesShouldInsertEntity() {
void insertIterableOfEntitiesShouldInsertEntity() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -231,7 +242,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void insertPublisherOfEntitiesShouldInsertEntity() {
void insertPublisherOfEntitiesShouldInsertEntity() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -247,7 +258,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void saveEntityShouldUpdateExistingEntity() {
void saveEntityShouldUpdateExistingEntity() {
dave.setFirstname("Hello, Dave");
dave.setLastname("Bowman");
@@ -264,7 +275,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void saveEntityShouldInsertNewEntity() {
void saveEntityShouldInsertNewEntity() {
ReactivePerson person = new ReactivePerson("Homer", "Simpson", 36);
@@ -278,7 +289,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void saveIterableOfNewEntitiesShouldInsertEntity() {
void saveIterableOfNewEntitiesShouldInsertEntity() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -294,7 +305,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void saveIterableOfMixedEntitiesShouldInsertEntity() {
void saveIterableOfMixedEntitiesShouldInsertEntity() {
ReactivePerson person = new ReactivePerson("Homer", "Simpson", 36);
@@ -310,7 +321,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void savePublisherOfEntitiesShouldInsertEntity() {
void savePublisherOfEntitiesShouldInsertEntity() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -325,8 +336,20 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
assertThat(boyd.getId()).isNotNull();
}
@Test // GH-3609
void savePublisherOfImmutableEntitiesShouldInsertEntity() {
immutableRepository.deleteAll().as(StepVerifier::create).verifyComplete();
immutableRepository.saveAll(Flux.just(keith)).as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.id).isNotNull();
}) //
.verifyComplete();
}
@Test // DATAMONGO-1444
public void deleteAllShouldRemoveEntities() {
void deleteAllShouldRemoveEntities() {
repository.deleteAll().as(StepVerifier::create).verifyComplete();
@@ -334,7 +357,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void deleteByIdShouldRemoveEntity() {
void deleteByIdShouldRemoveEntity() {
repository.deleteById(dave.id).as(StepVerifier::create).verifyComplete();
@@ -342,7 +365,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1712
public void deleteByIdUsingMonoShouldRemoveEntity() {
void deleteByIdUsingMonoShouldRemoveEntity() {
repository.deleteById(Mono.just(dave.id)).as(StepVerifier::create).verifyComplete();
@@ -350,7 +373,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1712
public void deleteByIdUsingFluxShouldRemoveEntity() {
void deleteByIdUsingFluxShouldRemoveEntity() {
repository.deleteById(Flux.just(dave.id, oliver.id)).as(StepVerifier::create).verifyComplete();
@@ -359,7 +382,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void deleteShouldRemoveEntity() {
void deleteShouldRemoveEntity() {
repository.delete(dave).as(StepVerifier::create).verifyComplete();
@@ -368,7 +391,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void deleteIterableOfEntitiesShouldRemoveEntities() {
void deleteIterableOfEntitiesShouldRemoveEntities() {
repository.deleteAll(Arrays.asList(dave, boyd)).as(StepVerifier::create).verifyComplete();
@@ -378,7 +401,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1444
public void deletePublisherOfEntitiesShouldRemoveEntities() {
void deletePublisherOfEntitiesShouldRemoveEntities() {
repository.deleteAll(Flux.just(dave, boyd)).as(StepVerifier::create).verifyComplete();
@@ -388,7 +411,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void findOneByExampleShouldReturnObject() {
void findOneByExampleShouldReturnObject() {
Example<ReactivePerson> example = Example.of(dave);
@@ -396,7 +419,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void findAllByExampleShouldReturnObjects() {
void findAllByExampleShouldReturnObjects() {
Example<ReactivePerson> example = Example.of(dave, matching().withIgnorePaths("id", "age", "firstname"));
@@ -404,7 +427,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void findAllByExampleAndSortShouldReturnObjects() {
void findAllByExampleAndSortShouldReturnObjects() {
Example<ReactivePerson> example = Example.of(dave, matching().withIgnorePaths("id", "age", "firstname"));
@@ -413,7 +436,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void countByExampleShouldCountObjects() {
void countByExampleShouldCountObjects() {
Example<ReactivePerson> example = Example.of(dave, matching().withIgnorePaths("id", "age", "firstname"));
@@ -421,7 +444,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void existsByExampleShouldReturnExisting() {
void existsByExampleShouldReturnExisting() {
Example<ReactivePerson> example = Example.of(dave, matching().withIgnorePaths("id", "age", "firstname"));
@@ -429,7 +452,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void existsByExampleShouldReturnNonExisting() {
void existsByExampleShouldReturnNonExisting() {
Example<ReactivePerson> example = Example.of(new ReactivePerson("foo", "bar", -1));
@@ -437,7 +460,7 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1619
public void findOneShouldEmitIncorrectResultSizeDataAccessExceptionWhenMoreThanOneElementFound() {
void findOneShouldEmitIncorrectResultSizeDataAccessExceptionWhenMoreThanOneElementFound() {
Example<ReactivePerson> example = Example.of(new ReactivePerson(null, "Matthews", -1),
matching().withIgnorePaths("age"));
@@ -446,19 +469,23 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
}
@Test // DATAMONGO-1907
public void findOneByExampleWithoutResultShouldCompleteEmpty() {
void findOneByExampleWithoutResultShouldCompleteEmpty() {
Example<ReactivePerson> example = Example.of(new ReactivePerson("foo", "bar", -1));
repository.findOne(example).as(StepVerifier::create).verifyComplete();
}
interface ReactivePersonRepostitory extends ReactiveMongoRepository<ReactivePerson, String> {
interface ReactivePersonRepository extends ReactiveMongoRepository<ReactivePerson, String> {
Flux<ReactivePerson> findByLastname(String lastname);
}
interface ReactiveImmutablePersonRepository extends ReactiveMongoRepository<ImmutableReactivePerson, String> {
}
@Data
@NoArgsConstructor
static class ReactivePerson {
@@ -469,11 +496,30 @@ public class SimpleReactiveMongoRepositoryTests implements BeanClassLoaderAware,
String lastname;
int age;
public ReactivePerson(String firstname, String lastname, int age) {
ReactivePerson(String firstname, String lastname, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
}
@With
@Value
static class ImmutableReactivePerson {
@Id String id;
String firstname;
String lastname;
int age;
ImmutableReactivePerson(@Nullable String id, String firstname, String lastname, int age) {
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
}
}

View File

@@ -1886,8 +1886,6 @@ AggregationResults<TagCount> results = template.aggregate(aggregation, "tags", T
WARNING: Indexes are only used if the collation used for the operation matches the index collation.
include::./mongo-json-schema.adoc[leveloffset=+1]
<<mongo.repositories>> support `Collations` via the `collation` attribute of the `@Query` annotation.
.Collation support for Repositories
@@ -1928,186 +1926,7 @@ as shown in (1) and (2), will be included when creating the index.
TIP: The most specifc `Collation` outroules potentially defined others. Which means Method argument over query method annotation over doamin type annotation.
====
[[mongo.jsonSchema]]
=== JSON Schema
As of version 3.6, MongoDB supports collections that validate documents against a provided https://docs.mongodb.com/manual/core/schema-validation/#json-schema[JSON Schema].
The schema itself and both validation action and level can be defined when creating the collection, as the following example shows:
.Sample JSON schema
====
[source,json]
----
{
"type": "object", <1>
"required": [ "firstname", "lastname" ], <2>
"properties": { <3>
"firstname": { <4>
"type": "string",
"enum": [ "luke", "han" ]
},
"address": { <5>
"type": "object",
"properties": {
"postCode": { "type": "string", "minLength": 4, "maxLength": 5 }
}
}
}
}
----
<1> JSON schema documents always describe a whole document from its root. A schema is a schema object itself that can contain
embedded schema objects that describe properties and subdocuments.
<2> `required` is a property that describes which properties are required in a document. It can be specified optionally, along with other
schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords].
<3> `properties` is related to a schema object that describes an `object` type. It contains property-specific schema constraints.
<4> `firstname` specifies constraints for the `firsname` field inside the document. Here, it is a string-based `properties` element declaring
possible field values.
<5> `address` is a subdocument defining a schema for values in its `postCode` field.
====
You can provide a schema either by specifying a schema document (that is, by using the `Document` API to parse or build a document object) or by building it with Spring Data's JSON schema utilities in `org.springframework.data.mongodb.core.schema`. `MongoJsonSchema` is the entry point for all JSON schema-related operations. The following example shows how use `MongoJsonSchema.builder()` to create a JSON schema:
.Creating a JSON schema
====
[source,java]
----
MongoJsonSchema.builder() <1>
.required("firstname", "lastname") <2>
.properties(
string("firstname").possibleValues("luke", "han"), <3>
object("address")
.properties(string("postCode").minLength(4).maxLength(5)))
.build(); <4>
----
<1> Obtain a schema builder to configure the schema with a fluent API.
<2> Configure required properties.
<3> Configure the String-typed `firstname` field, allowing only `luke` and `han` values. Properties can be typed or untyped. Use a static import of `JsonSchemaProperty` to make the syntax slightly more compact and to get entry points such as `string(…)`.
<4> Build the schema object. Use the schema to create either a collection or <<mongodb-template-query.criteria,query documents>>.
====
There are already some predefined and strongly typed schema objects (`JsonSchemaObject` and `JsonSchemaProperty`) available
through static methods on the gateway interfaces.
However, you may need to build custom property validation rules, which can be created through the builder API, as the following example shows:
[source,java]
----
// "birthdate" : { "bsonType": "date" }
JsonSchemaProperty.named("birthdate").ofType(Type.dateType());
// "birthdate" : { "bsonType": "date", "description", "Must be a date" }
JsonSchemaProperty.named("birthdate").with(JsonSchemaObject.of(Type.dateType()).description("Must be a date"));
----
The Schema builder also provides support for https://docs.mongodb.com/manual/core/security-client-side-encryption/[Client-Side Field Level Encryption]. Please refer to <<mongo.jsonSchema.encrypted-fields>> for more information,
`CollectionOptions` provides the entry point to schema support for collections, as the following example shows:
.Create collection with `$jsonSchema`
====
[source,java]
----
MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
template.createCollection(Person.class, CollectionOptions.empty().schema(schema));
----
====
You can use a schema to query any collection for documents that match a given structure defined by a JSON schema, as the following example shows:
.Query for Documents matching a `$jsonSchema`
====
[source,java]
----
MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build();
template.find(query(matchingDocumentStructure(schema)), Person.class);
----
====
The following table shows the supported JSON schema types:
[cols="3,1,6", options="header"]
.Supported JSON schema types
|===
| Schema Type
| Java Type
| Schema Properties
| `untyped`
| -
| `description`, generated `description`, `enum`, `allOf`, `anyOf`, `oneOf`, `not`
| `object`
| `Object`
| `required`, `additionalProperties`, `properties`, `minProperties`, `maxProperties`, `patternProperties`
| `array`
| any array except `byte[]`
| `uniqueItems`, `additionalItems`, `items`, `minItems`, `maxItems`
| `string`
| `String`
| `minLength`, `maxLentgth`, `pattern`
| `int`
| `int`, `Integer`
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
| `long`
| `long`, `Long`
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
| `double`
| `float`, `Float`, `double`, `Double`
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
| `decimal`
| `BigDecimal`
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
| `number`
| `Number`
| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`
| `binData`
| `byte[]`
| (none)
| `boolean`
| `boolean`, `Boolean`
| (none)
| `null`
| `null`
| (none)
| `objectId`
| `ObjectId`
| (none)
| `date`
| `java.util.Date`
| (none)
| `timestamp`
| `BsonTimestamp`
| (none)
| `regex`
| `java.util.regex.Pattern`
| (none)
|===
NOTE: `untyped` is a generic type that is inherited by all typed schema types. It provides all `untyped` schema properties to typed schema types.
For more information, see https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema[$jsonSchema].
include::./mongo-json-schema.adoc[leveloffset=+1]
[[mongo.query.fluent-template-api]]
=== Fluent Template API

View File

@@ -1,6 +1,86 @@
Spring Data MongoDB Changelog
=============================
Changes in version 3.1.7 (2021-03-31)
-------------------------------------
* #3613 - Use StringUtils.replace(…) instead of String.replaceAll(…) for mapKeyDotReplacement.
* #3609 - SimpleReactiveMongoRepository#saveAll does not populate @Id property if it is immutable.
Changes in version 3.2.0-RC1 (2021-03-31)
-----------------------------------------
* #3613 - Use StringUtils.replace(…) instead of String.replaceAll(…) for mapKeyDotReplacement.
* #3609 - SimpleReactiveMongoRepository#saveAll does not populate @Id property if it is immutable.
* #3600 - Rename Embedded annotation -> Unwrapped.
* #3583 - Support aggregation expression on fields projection.
Changes in version 3.2.0-M5 (2021-03-17)
----------------------------------------
* #3592 - Remove @Persistent from entity-scan include filters.
* #3590 - Embedded sharding keys are not correctly picked up from the shardKeySource Document.
* #3580 - Fix CustomConverter conversion lookup.
* #3579 - Upgrade to MongoDB Java Drivers 4.2.2.
* #3575 - Introduce ConversionContext and clean up MappingMongoConverter.
* #3573 - Json Schema section appears twice in reference documentation.
* #3571 - Introduce ConversionContext and clean up MappingMongoConverter.
* #3570 - Incorrect class casting cause ClassCastException when save java.util.Collection using MongoTemplate.
* #3568 - MongoSocketWriteException may be translated into DataAccessResourceFailureException.
* #3566 - Couldn't find PersistentEntity for type java.lang.Object when updating a field with suffix "class".
* #3552 - UpdateMapper drops numeric keys in Maps.
* #3395 - Derived findBy…IgnoreCaseIn query doesn't return expected results [DATAMONGO-2540].
* #3286 - Add possibility to use Collection<Criteria> as parameter in and/or/nor operators [DATAMONGO-2428].
* #2911 - ensureNotIterable in MongoTemplate only checks for array type [DATAMONGO-2044].
* #590 - DATAMONGO-2044 make ensureNotIterable actually check if object is iterable.
Changes in version 3.1.6 (2021-03-17)
-------------------------------------
* #3592 - Remove @Persistent from entity-scan include filters.
* #3590 - Embedded sharding keys are not correctly picked up from the shardKeySource Document.
* #3589 - Upgrade to MongoDB Driver 4.1.2.
* #3573 - Json Schema section appears twice in reference documentation.
* #3568 - MongoSocketWriteException may be translated into DataAccessResourceFailureException.
* #3566 - Couldn't find PersistentEntity for type java.lang.Object when updating a field with suffix "class".
* #3552 - UpdateMapper drops numeric keys in Maps.
* #3395 - Derived findBy…IgnoreCaseIn query doesn't return expected results [DATAMONGO-2540].
Changes in version 3.0.8.RELEASE (2021-03-17)
---------------------------------------------
* #3590 - Embedded sharding keys are not correctly picked up from the shardKeySource Document.
* #3588 - Upgrade to MongoDB Driver 4.0.6.
* #3573 - Json Schema section appears twice in reference documentation.
* #3568 - MongoSocketWriteException may be translated into DataAccessResourceFailureException.
* #3566 - Couldn't find PersistentEntity for type java.lang.Object when updating a field with suffix "class".
* #3552 - UpdateMapper drops numeric keys in Maps.
* #3395 - Derived findBy…IgnoreCaseIn query doesn't return expected results [DATAMONGO-2540].
Changes in version 3.2.0-M4 (2021-02-18)
----------------------------------------
Changes in version 3.1.5 (2021-02-18)
-------------------------------------
Changes in version 3.2.0-M3 (2021-02-17)
----------------------------------------
* #3553 - Upgrade to MongoDB driver 4.2.0.
* #3546 - org.bson.codecs.configuration.CodecConfigurationException: The uuidRepresentation has not been specified, so the UUID cannot be encoded.
* #3544 - alike Criteria can't add andOperator.
* #3542 - Relax field name checks for TypedAggregations.
* #3540 - Allow access to mongoDatabaseFactory used in ReactiveMongoTemplate.
* #3529 - Update repository after GitHub issues migration.
* #3525 - Bug in full text query documentation [DATAMONGO-2673].
* #3517 - GeoJson: Improper Deserialization of Document with a GeoJsonPolygon [DATAMONGO-2664].
* #3508 - Add ReactiveMongoOperations.aggregate(…) Kotlin extension [DATAMONGO-2655].
* #3474 - Search by alike() criteria is broken when type alias information is not available [DATAMONGO-2620].
* #3055 - Improve count() and countDocuments() mapping documentation and/or method availability [DATAMONGO-2192].
* #2803 - Support flattening embedded/nested objects [DATAMONGO-1902].
Changes in version 3.1.4 (2021-02-17)
-------------------------------------
* #3546 - org.bson.codecs.configuration.CodecConfigurationException: The uuidRepresentation has not been specified, so the UUID cannot be encoded.
@@ -3308,6 +3388,14 @@ Repository

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 3.1.4 (2020.0.4)
Spring Data MongoDB 3.1.7 (2020.0.7)
Copyright (c) [2010-2019] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -24,3 +24,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.