DATAMONGO-2264 - Support upcoming MongoDB 4.2 Server.

Deprecate and guard tests for removed commands:
    - eval
    - group
    - maxScan
    - geoNear

Transition from removed geoNear command to $geoNear aggregation pipeline stage.
This allows users to still use NearQuery along with the template that now transforms the query into a GeoNearOperation applying skip (previously in memory skipping) and limit (previous num parameter) as additional aggregation pipeline stages.

Tested against:
    - 4.1.10
    - 4.0.4
    - 3.6.12
    - 4.4.20

Original pull request: #744.
This commit is contained in:
Christoph Strobl
2019-04-30 15:37:43 +02:00
committed by Mark Paluch
parent e683c8f08f
commit 33d69abd53
39 changed files with 647 additions and 259 deletions

View File

@@ -16,9 +16,12 @@ before_install:
env:
matrix:
- PROFILE=ci
- MONGO_VERSION=4.1.10
- MONGO_VERSION=4.0.4
- MONGO_VERSION=3.6.12
- MONGO_VERSION=3.4.20
global:
- MONGO_VERSION=4.0.0
- PROFILE=ci
addons:
apt:

View File

@@ -42,13 +42,15 @@ import com.mongodb.MongoException;
import com.mongodb.client.MongoDatabase;
/**
* Default implementation of {@link ScriptOperations} capable of saving and executing {@link ServerSideJavaScript}.
* Default implementation of {@link ScriptOperations} capable of saving and executing {@link ExecutableMongoScript}.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.7
* @deprecated since 2.2. The {@code eval} command has been removed in MongoDB Server 4.2.0.
*/
@Deprecated
class DefaultScriptOperations implements ScriptOperations {
private static final String SCRIPT_COLLECTION_NAME = "system.js";

View File

@@ -24,7 +24,6 @@ import java.util.Map;
import java.util.Optional;
import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.IdentifierAccessor;
@@ -45,6 +44,7 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* Common operations performed on an entity in the context of it's mapping metadata.
@@ -164,6 +164,28 @@ class EntityOperations {
return ID_FIELD;
}
/**
* Return the name used for {@code $geoNear.distanceField} avoiding clashes with potentially existing properties.
*
* @param domainType must not be {@literal null}.
* @return the name of the distanceField to use. {@literal dis} by default.
* @since 2.2
*/
public String nearQueryDistanceFieldName(Class<?> domainType) {
if (!context.hasPersistentEntityFor(domainType)
|| context.getPersistentEntity(domainType).getPersistentProperty("dis") == null) {
return "dis";
}
String distanceFieldName = "calculated-distance";
while (context.getPersistentEntity(domainType).getPersistentProperty(distanceFieldName) != null) {
distanceFieldName += "-" + ObjectUtils.getIdentityHexString(new Object());
}
return distanceFieldName;
}
private static Document parse(String source) {
try {

View File

@@ -355,7 +355,9 @@ public interface MongoOperations extends FluentMongoOperations {
*
* @return
* @since 1.7
* @deprecated since 2.2. The {@code eval} command has been removed without replacement in MongoDB Server 4.2.0.
*/
@Deprecated
ScriptOperations scriptOps();
/**
@@ -427,7 +429,11 @@ public interface MongoOperations extends FluentMongoOperations {
* reduce function.
* @param entityClass The parametrized type of the returned list
* @return The results of the group operation
* @deprecated since 2.2. The {@code group} command has been removed in MongoDB Server 4.2.0. <br />
* Please use {@link #aggregate(TypedAggregation, String, Class) } with a
* {@link org.springframework.data.mongodb.core.aggregation.GroupOperation} instead.
*/
@Deprecated
<T> GroupByResults<T> group(String inputCollectionName, GroupBy groupBy, Class<T> entityClass);
/**
@@ -442,7 +448,12 @@ public interface MongoOperations extends FluentMongoOperations {
* reduce function.
* @param entityClass The parametrized type of the returned list
* @return The results of the group operation
* @deprecated since 2.2. The {@code group} command has been removed in MongoDB Server 4.2.0. <br />
* Please use {@link #aggregate(TypedAggregation, String, Class) } with a
* {@link org.springframework.data.mongodb.core.aggregation.GroupOperation} and
* {@link org.springframework.data.mongodb.core.aggregation.MatchOperation} instead.
*/
@Deprecated
<T> GroupByResults<T> group(@Nullable Criteria criteria, String inputCollectionName, GroupBy groupBy,
Class<T> entityClass);
@@ -1147,8 +1158,8 @@ public interface MongoOperations extends FluentMongoOperations {
* If you object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's Type
* Conversion"</a> for more details.
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
* <p/>
* <p/>
* Insert is used to initially store the object into the database. To update an existing object use the save method.
@@ -1209,8 +1220,8 @@ public interface MongoOperations extends FluentMongoOperations {
* If you object has an "Id' property, it will be set with the generated Id from MongoDB. If your Id property is a
* String then MongoDB ObjectId will be used to populate that string. Otherwise, the conversion from ObjectId to your
* property type will be handled by Spring's BeanWrapper class that leverages Type Conversion API. See
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's Type
* Conversion"</a> for more details.
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation" > Spring's
* Type Conversion"</a> for more details.
*
* @param objectToSave the object to store in the collection. Must not be {@literal null}.
* @return the saved object.

View File

@@ -23,18 +23,9 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -44,7 +35,6 @@ import org.bson.codecs.Codec;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -126,6 +116,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
@@ -148,17 +139,7 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
import com.mongodb.client.model.*;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
@@ -989,7 +970,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return geoNear(near, domainType, collectionName, domainType);
}
@SuppressWarnings("unchecked")
public <T> GeoResults<T> geoNear(NearQuery near, Class<?> domainType, String collectionName, Class<T> returnType) {
if (near == null) {
@@ -1005,53 +985,31 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
String collection = StringUtils.hasText(collectionName) ? collectionName
: operations.determineCollectionName(domainType);
Document nearDocument = near.toDocument();
String distanceField = operations.nearQueryDistanceFieldName(domainType);
Document command = new Document("geoNear", collection);
command.putAll(queryMapper.getMappedObject(nearDocument, Optional.empty()));
Aggregation $geoNear = TypedAggregation.newAggregation(domainType, Aggregation.geoNear(near, distanceField))
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
if (nearDocument.containsKey("query")) {
Document query = (Document) nearDocument.get("query");
command.put("query", queryMapper.getMappedObject(query, getPersistentEntity(domainType)));
AggregationResults<Document> results = aggregate($geoNear, collection, Document.class);
DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<>(distanceField,
new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric());
List<GeoResult<T>> result = new ArrayList<>();
BigDecimal aggregate = BigDecimal.ZERO;
for (Document element : results) {
GeoResult<T> geoResult = callback.doWith(element);
aggregate = aggregate.add(new BigDecimal(geoResult.getDistance().getValue()));
result.add(geoResult);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing geoNear using: {} for class: {} in collection: {}", serializeToJsonSafely(command),
domainType, collectionName);
}
Distance avgDistance = new Distance(
result.size() == 0 ? 0 : aggregate.divide(new BigDecimal(result.size()), RoundingMode.HALF_UP).doubleValue(),
near.getMetric());
Document commandResult = executeCommand(command, this.readPreference);
List<Object> results = (List<Object>) commandResult.get("results");
results = results == null ? Collections.emptyList() : results;
DocumentCallback<GeoResult<T>> callback = new GeoNearResultDocumentCallback<>(
new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collectionName), near.getMetric());
List<GeoResult<T>> result = new ArrayList<>(results.size());
int index = 0;
long elementsToSkip = near.getSkip() != null ? near.getSkip() : 0;
for (Object element : results) {
/*
* As MongoDB currently (2.4.4) doesn't support the skipping of elements in near queries
* we skip the elements ourselves to avoid at least the document 2 object mapping overhead.
*
* @see <a href="https://jira.mongodb.org/browse/SERVER-3925">MongoDB Jira: SERVER-3925</a>
*/
if (index >= elementsToSkip) {
result.add(callback.doWith((Document) element));
}
index++;
}
if (elementsToSkip > 0) {
// as we skipped some elements we have to calculate the averageDistance ourselves:
return new GeoResults<>(result, near.getMetric());
}
GeoCommandStatistics stats = GeoCommandStatistics.from(commandResult);
return new GeoResults<>(result, new Distance(stats.getAverageDistance(), near.getMetric()));
return new GeoResults<>(result, avgDistance);
}
@Nullable
@@ -3314,9 +3272,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* a delegate and creates a {@link GeoResult} from the result.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
static class GeoNearResultDocumentCallback<T> implements DocumentCallback<GeoResult<T>> {
private final String distanceField;
private final DocumentCallback<T> delegate;
private final Metric metric;
@@ -3324,12 +3284,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* Creates a new {@link GeoNearResultDocumentCallback} using the given {@link DocumentCallback} delegate for
* {@link GeoResult} content unmarshalling.
*
* @param distanceField the field to read the distance from.
* @param delegate must not be {@literal null}.
* @param metric the {@link Metric} to apply to the result distance.
*/
public GeoNearResultDocumentCallback(DocumentCallback<T> delegate, Metric metric) {
GeoNearResultDocumentCallback(String distanceField, DocumentCallback<T> delegate, Metric metric) {
Assert.notNull(delegate, "DocumentCallback must not be null!");
this.distanceField = distanceField;
this.delegate = delegate;
this.metric = metric;
}
@@ -3337,10 +3300,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
@Nullable
public GeoResult<T> doWith(@Nullable Document object) {
double distance = ((Double) object.get("dis")).doubleValue();
Document content = (Document) object.get("obj");
double distance = Double.NaN;
if (object.containsKey(distanceField)) {
T doWith = delegate.doWith(content);
distance = NumberUtils.convertNumberToTargetClass(object.get(distanceField, Number.class), Double.class)
.doubleValue();
}
T doWith = delegate.doWith(object);
return new GeoResult<>(doWith, new Distance(distance, metric));
}

View File

@@ -20,22 +20,13 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.util.NumberUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -50,7 +41,6 @@ import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -1060,34 +1050,16 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(entityClass);
Document nearDocument = near.toDocument();
String distanceField = operations.nearQueryDistanceFieldName(entityClass);
Document command = new Document("geoNear", collection);
command.putAll(nearDocument);
GeoNearResultDocumentCallback<T> callback = new GeoNearResultDocumentCallback<>(distanceField,
new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collection), near.getMetric());
return Flux.defer(() -> {
Aggregation $geoNear = TypedAggregation.newAggregation(entityClass, Aggregation.geoNear(near, "dis"))
.withOptions(AggregationOptions.builder().collation(near.getCollation()).build());
if (nearDocument.containsKey("query")) {
Document query = (Document) nearDocument.get("query");
command.put("query", queryMapper.getMappedObject(query, getPersistentEntity(entityClass)));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing geoNear using: {} for class: {} in collection: {}", serializeToJsonSafely(command),
entityClass, collectionName);
}
GeoNearResultDocumentCallback<T> callback = new GeoNearResultDocumentCallback<>(
new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collectionName), near.getMetric());
return executeCommand(command, this.readPreference).flatMapMany(document -> {
List<Document> results = document.get("results", List.class);
return results == null ? Flux.empty() : Flux.fromIterable(results);
}).skip(near.getSkip() != null ? near.getSkip() : 0).map(callback::doWith);
});
return aggregate($geoNear, collection, Document.class) //
.map(callback::doWith);
}
/*
@@ -3039,9 +3011,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* a delegate and creates a {@link GeoResult} from the result.
*
* @author Mark Paluch
* @author Chrstoph Strobl
*/
static class GeoNearResultDocumentCallback<T> implements DocumentCallback<GeoResult<T>> {
private final String distanceField;
private final DocumentCallback<T> delegate;
private final Metric metric;
@@ -3049,22 +3023,29 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* Creates a new {@link GeoNearResultDocumentCallback} using the given {@link DocumentCallback} delegate for
* {@link GeoResult} content unmarshalling.
*
* @param distanceField the field to read the distance from.
* @param delegate must not be {@literal null}.
* @param metric the {@link Metric} to apply to the result distance.
*/
GeoNearResultDocumentCallback(DocumentCallback<T> delegate, Metric metric) {
GeoNearResultDocumentCallback(String distanceField, DocumentCallback<T> delegate, Metric metric) {
Assert.notNull(delegate, "DocumentCallback must not be null!");
this.distanceField = distanceField;
this.delegate = delegate;
this.metric = metric;
}
public GeoResult<T> doWith(Document object) {
double distance = (Double) object.get("dis");
Document content = (Document) object.get("obj");
double distance = Double.NaN;
if (object.containsKey(distanceField)) {
T doWith = delegate.doWith(content);
distance = NumberUtils.convertNumberToTargetClass(object.get(distanceField, Number.class), Double.class)
.doubleValue();
}
T doWith = delegate.doWith(object);
return new GeoResult<>(doWith, new Distance(distance, metric));
}

View File

@@ -29,7 +29,9 @@ import com.mongodb.DB;
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.7
* @deprecated since 2.2. The {@code eval} command has been removed without replacement in MongoDB Server 4.2.0.
*/
@Deprecated
public interface ScriptOperations {
/**

View File

@@ -612,7 +612,7 @@ public class Aggregation {
}
/**
* Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the{@code distanceField}. The
* Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the {@code distanceField}. The
* {@code distanceField} defines output field that contains the calculated distance.
*
* @param query must not be {@literal null}.

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.Collections;
import java.util.List;
import org.bson.Document;
/**
@@ -32,7 +35,23 @@ public interface AggregationOperation {
* Turns the {@link AggregationOperation} into a {@link Document} by using the given
* {@link AggregationOperationContext}.
*
* @param context the {@link AggregationOperationContext} to operate within. Must not be {@literal null}.
* @return the Document
* @deprecated since 2.2 in favor of {@link #toPipelineStages(AggregationOperationContext)}.
*/
@Deprecated
Document toDocument(AggregationOperationContext context);
/**
* Turns the {@link AggregationOperation} into list of {@link Document stages} by using the given
* {@link AggregationOperationContext}. This allows a single {@link AggregationOptions} to add additional stages for
* eg. {@code $sort} or {@code $limit}.
*
* @param context the {@link AggregationOperationContext} to operate within. Must not be {@literal null}.
* @return the pipeline stages to run through. Never {@literal null}.
* @since 2.2
*/
default List<Document> toPipelineStages(AggregationOperationContext context) {
return Collections.singletonList(toDocument(context));
}
}

View File

@@ -52,7 +52,7 @@ class AggregationOperationRenderer {
for (AggregationOperation operation : operations) {
operationDocuments.add(operation.toDocument(contextToUse));
operationDocuments.addAll(operation.toPipelineStages(contextToUse));
if (operation instanceof FieldsExposingAggregationOperation) {

View File

@@ -15,10 +15,15 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.bson.Document;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
@@ -89,6 +94,15 @@ public class GeoNearOperation implements AggregationOperation {
public Document toDocument(AggregationOperationContext context) {
Document command = context.getMappedObject(nearQuery.toDocument());
if(command.containsKey("query")) {
command.replace("query", context.getMappedObject(command.get("query", Document.class)));
}
if(command.containsKey("collation")) {
command.remove("collation");
}
command.put("distanceField", distanceField);
if (StringUtils.hasText(indexKey)) {
@@ -97,4 +111,28 @@ public class GeoNearOperation implements AggregationOperation {
return new Document("$geoNear", command);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toPipelineStages(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public List<Document> toPipelineStages(AggregationOperationContext context) {
Document command = toDocument(context);
Number limit = (Number) command.get("$geoNear", Document.class).remove("num");
List<Document> stages = new ArrayList<>();
stages.add(command);
if(nearQuery.getSkip() != null && nearQuery.getSkip() > 0){
stages.add(new Document("$skip", nearQuery.getSkip()));
}
if(limit != null) {
stages.add(new Document("$limit", limit.longValue()));
}
return stages;
}
}

View File

@@ -128,7 +128,13 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
*/
@Override
public ExposedFields getFields() {
return ExposedFields.from(new ExposedField(as, true));
List<ExposedField> fields = new ArrayList<>(2);
fields.add(new ExposedField(as, true));
if(depthField != null) {
fields.add(new ExposedField(depthField, true));
}
return ExposedFields.from(fields.toArray(new ExposedField[0]));
}
/**

View File

@@ -79,8 +79,6 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
*/
@Override
public FieldReference getReference(Field field) {
PropertyPath.from(field.getTarget(), type);
return getReferenceFor(field);
}

View File

@@ -29,7 +29,9 @@ import org.springframework.lang.Nullable;
* @author Mark Pollack
* @author Christoph Strobl
* @author Mark Paluch
* @deprecated since 2.2. The {@code group} command has been removed in MongoDB Server 4.2.0.
*/
@Deprecated
public class GroupBy {
private @Nullable Document initialDocument;

View File

@@ -29,7 +29,9 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Mark Paluch
* @param <T> The class in which the results are mapped onto, accessible via an {@link Iterator}.
* @deprecated since 2.2. The {@code group} command has been removed in MongoDB Server 4.2.0.
*/
@Deprecated
public class GroupByResults<T> implements Iterable<T> {
private final List<T> mappedResults;

View File

@@ -110,8 +110,10 @@ public class Meta {
/**
* @return {@literal null} if not set.
* @deprecated since 2.2. {@code $maxScan} has been removed without replacement in MongoDB 4.2.
*/
@Nullable
@Deprecated
public Long getMaxScan() {
return getValue(MetaKey.MAX_SCAN.key);
}
@@ -120,7 +122,7 @@ public class Meta {
* Only scan the specified number of documents.
*
* @param maxScan
* @deprecated since 2.1 due to deprecation in MongoDB 4.0.
* @deprecated since 2.1. {@code $maxScan} has been removed without replacement in MongoDB 4.2.
*/
@Deprecated
public void setMaxScan(long maxScan) {

View File

@@ -179,7 +179,7 @@ public final class NearQuery {
private @Nullable Distance minDistance;
private Metric metric;
private boolean spherical;
private @Nullable Long num;
private @Nullable Long limit;
private @Nullable Long skip;
/**
@@ -269,9 +269,22 @@ public final class NearQuery {
*
* @param num
* @return
* @deprecated since 2.2. Please use {@link #limit(long)} instead.
*/
@Deprecated
public NearQuery num(long num) {
this.num = num;
return limit(num);
}
/**
* Configures the maximum number of results to return.
*
* @param limit
* @return
* @since 2.2
*/
public NearQuery limit(long limit) {
this.limit = limit;
return this;
}
@@ -296,8 +309,8 @@ public final class NearQuery {
Assert.notNull(pageable, "Pageable must not be 'null'.");
if (pageable.isPaged()) {
this.num = pageable.getOffset() + pageable.getPageSize();
this.skip = pageable.getOffset();
this.limit = (long) pageable.getPageSize();
}
return this;
}
@@ -530,7 +543,7 @@ public final class NearQuery {
this.skip = query.getSkip();
if (query.getLimit() != 0) {
this.num = (long) query.getLimit();
this.limit = (long) query.getLimit();
}
return this;
}
@@ -543,6 +556,17 @@ public final class NearQuery {
return skip;
}
/**
* Get the {@link Collation} to use along with the {@link #query(Query)}.
*
* @return the {@link Collation} if set. {@literal null} otherwise.
* @since 2.2
*/
@Nullable
public Collation getCollation() {
return query != null ? query.getCollation().orElse(null) : null;
}
/**
* Returns the {@link Document} built by the {@link NearQuery}.
*
@@ -570,8 +594,8 @@ public final class NearQuery {
document.put("distanceMultiplier", getDistanceMultiplier());
}
if (num != null) {
document.put("num", num);
if (limit != null) {
document.put("num", limit);
}
if (usesGeoJson()) {

View File

@@ -23,7 +23,9 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.7
* @deprecated since 2.2. The {@code eval} command has been removed without replacement in MongoDB Server 4.2.0.
*/
@Deprecated
public class ExecutableMongoScript {
private final String code;

View File

@@ -25,7 +25,9 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.7
* @deprecated since 2.2. The {@code eval} command has been removed without replacement in MongoDB Server 4.2.0.
*/
@Deprecated
public class NamedMongoScript {
private final @Id String name;

View File

@@ -19,6 +19,7 @@ import org.bson.Document;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.support.MetricType;
import org.springframework.util.NumberUtils;
import com.mongodb.MongoClient;
@@ -66,6 +67,6 @@ public class OperationCounters extends AbstractMonitor {
private int getOpCounter(String key) {
Document opCounters = (Document) getServerStatus().get("opcounters");
return (Integer) opCounters.get(key);
return NumberUtils.convertNumberToTargetClass((Number) opCounters.get(key), Integer.class);
}
}

View File

@@ -46,7 +46,9 @@ public @interface Meta {
* Only scan the specified number of documents.
*
* @return
* @deprecated since 2.2. {@code $maxScan} has been removed without replacement in MongoDB 4.2.
*/
@Deprecated
long maxScanDocuments() default -1;
/**

View File

@@ -21,10 +21,10 @@ import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
@@ -37,6 +37,8 @@ import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
import com.mongodb.MongoException;
/**
* {@link QueryCreationListener} inspecting {@link PartTreeMongoQuery}s and creating an index for the properties it
* refers to.
@@ -104,7 +106,26 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
}
MongoEntityMetadata<?> metadata = query.getQueryMethod().getEntityInformation();
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
try {
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
} catch (UncategorizedMongoDbException e) {
if (e.getCause() instanceof MongoException) {
/*
* As of MongoDB 4.2 index creation raises an error when creating an index for the very same keys with
* different name, whereas previous versions silently ignored this.
* Because an index is by default named after the repository finder method it is not uncommon that an index
* for the very same property combination might already exist with a different name.
* So you see, that's why we need to ignore the error here.
*
* For details please see: https://docs.mongodb.com/master/release-notes/4.2-compatibility/#indexes
*/
if (((MongoException) e.getCause()).getCode() != 85) {
throw e;
}
}
}
LOG.debug(String.format("Created %s!", index));
}

View File

@@ -23,6 +23,7 @@ import static org.springframework.data.mongodb.core.query.Query.*;
import org.bson.Document;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -32,6 +33,8 @@ import org.springframework.dao.UncategorizedDataAccessException;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.script.ExecutableMongoScript;
import org.springframework.data.mongodb.core.script.NamedMongoScript;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -48,6 +51,8 @@ import com.mongodb.MongoClient;
@ContextConfiguration
public class DefaultScriptOperationsTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_MOST_4_0 = MongoVersionRule.atMost(Version.parse("4.0.999"));
@Configuration
static class Config {

View File

@@ -27,6 +27,7 @@ import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.core.query.Update.*;
import com.mongodb.MongoClient;
import com.mongodb.client.model.IndexOptions;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -45,6 +46,7 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.assertj.core.api.Assertions;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.hamcrest.collection.IsMapContaining;
import org.joda.time.DateTime;
@@ -415,16 +417,15 @@ public class MongoTemplateTests {
assertThat(field, is(IndexField.create("age", Direction.DESC)));
}
@Test // DATAMONGO-746
@Test // DATAMONGO-746, DATAMONGO-2264
public void testReadIndexInfoForIndicesCreatedViaMongoShellCommands() throws Exception {
String command = "db." + template.getCollectionName(Person.class)
+ ".createIndex({'age':-1}, {'unique':true, 'sparse':true}), 1";
template.indexOps(Person.class).dropAllIndexes();
assertThat(template.indexOps(Person.class).getIndexInfo().isEmpty(), is(true));
factory.getDb().runCommand(new org.bson.Document("eval", command));
factory.getDb().getCollection(template.getCollectionName(Person.class))
.createIndex(new org.bson.Document("age", -1), new IndexOptions().name("age_-1").unique(true).sparse(true));
ListIndexesIterable<org.bson.Document> indexInfo = template.getCollection(template.getCollectionName(Person.class))
.listIndexes();
@@ -443,7 +444,7 @@ public class MongoTemplateTests {
}
}
assertThat(indexKey, IsMapContaining.<String, Object> hasEntry("age", -1D));
assertThat(indexKey, IsMapContaining.<String, Object> hasEntry("age", -1));
assertThat(unique, is(true));
IndexInfo info = template.indexOps(Person.class).getIndexInfo().get(1);

View File

@@ -425,29 +425,24 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(collection, never()).withReadPreference(any());
}
@Test // DATAMONGO-1166
@Test // DATAMONGO-1166, DATAMONGO-2264
public void geoNearShouldHonorReadPreferenceWhenSet() {
when(db.runCommand(any(org.bson.Document.class), any(ReadPreference.class), eq(Document.class)))
.thenReturn(mock(Document.class));
template.setReadPreference(ReadPreference.secondary());
NearQuery query = NearQuery.near(new Point(1, 1));
template.geoNear(query, Wrapper.class);
verify(this.db, times(1)).runCommand(any(org.bson.Document.class), eq(ReadPreference.secondary()),
eq(Document.class));
verify(collection).withReadPreference(eq(ReadPreference.secondary()));
}
@Test // DATAMONGO-1166
@Test // DATAMONGO-1166, DATAMONGO-2264
public void geoNearShouldIgnoreReadPreferenceWhenNotSet() {
when(db.runCommand(any(Document.class), eq(Document.class))).thenReturn(mock(Document.class));
NearQuery query = NearQuery.near(new Point(1, 1));
template.geoNear(query, Wrapper.class);
verify(this.db, times(1)).runCommand(any(Document.class), eq(Document.class));
verify(collection, never()).withReadPreference(any());
}
@Test // DATAMONGO-1334
@@ -886,16 +881,13 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(mapReduceIterable, never()).databaseName(any());
}
@Test // DATAMONGO-1518
@Test // DATAMONGO-1518, DATAMONGO-2264
public void geoNearShouldUseCollationWhenPresent() {
NearQuery query = NearQuery.near(0D, 0D).query(new BasicQuery("{}").collation(Collation.of("fr")));
template.geoNear(query, AutogenerateableId.class);
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
verify(aggregateIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
@@ -980,38 +972,38 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(findIterable).projection(eq(new Document()));
}
@Test // DATAMONGO-1348
@Test // DATAMONGO-1348, DATAMONGO-2264
public void geoNearShouldMapQueryCorrectly() {
when(db.runCommand(any(Document.class), eq(Document.class))).thenReturn(mock(Document.class));
NearQuery query = NearQuery.near(new Point(1, 1));
query.query(Query.query(Criteria.where("customName").is("rand al'thor")));
template.geoNear(query, WithNamedFields.class);
ArgumentCaptor<Document> capture = ArgumentCaptor.forClass(Document.class);
verify(this.db, times(1)).runCommand(capture.capture(), any(Class.class));
ArgumentCaptor<List<Document>> capture = ArgumentCaptor.forClass(List.class);
assertThat(capture.getValue(), IsBsonObject.isBsonObject().containing("query.custom-named-field", "rand al'thor")
verify(collection).aggregate(capture.capture(), eq(Document.class));
Document $geoNear = capture.getValue().iterator().next();
assertThat($geoNear, IsBsonObject.isBsonObject().containing("$geoNear.query.custom-named-field", "rand al'thor")
.notContaining("query.customName"));
}
@Test // DATAMONGO-1348
@Test // DATAMONGO-1348, DATAMONGO-2264
public void geoNearShouldMapGeoJsonPointCorrectly() {
when(db.runCommand(any(Document.class), eq(Document.class))).thenReturn(mock(Document.class));
NearQuery query = NearQuery.near(new GeoJsonPoint(1, 2));
query.query(Query.query(Criteria.where("customName").is("rand al'thor")));
template.geoNear(query, WithNamedFields.class);
ArgumentCaptor<Document> capture = ArgumentCaptor.forClass(Document.class);
verify(this.db, times(1)).runCommand(capture.capture(), any(Class.class));
ArgumentCaptor<List<Document>> capture = ArgumentCaptor.forClass(List.class);
assertThat(capture.getValue(), IsBsonObject.isBsonObject().containing("near.type", "Point")
.containing("near.coordinates.[0]", 1D).containing("near.coordinates.[1]", 2D));
verify(collection).aggregate(capture.capture(), eq(Document.class));
Document $geoNear = capture.getValue().iterator().next();
assertThat($geoNear, IsBsonObject.isBsonObject().containing("$geoNear.near.type", "Point")
.containing("$geoNear.near.coordinates.[0]", 1D).containing("$geoNear.near.coordinates.[1]", 2D));
}
@Test // DATAMONGO-2155

View File

@@ -44,6 +44,7 @@ import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.reactivestreams.client.ListIndexesPublisher;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.Success;
@@ -140,12 +141,9 @@ public class ReactiveMongoTemplateIndexTests {
}).verifyComplete();
}
@Test // DATAMONGO-1444
@Test // DATAMONGO-1444, DATAMONGO-2264
public void testReadIndexInfoForIndicesCreatedViaMongoShellCommands() {
String command = "db." + template.getCollectionName(Person.class)
+ ".createIndex({'age':-1}, {'unique':true, 'sparse':true}), 1";
template.indexOps(Person.class).dropAllIndexes() //
.as(StepVerifier::create) //
.verifyComplete();
@@ -154,7 +152,8 @@ public class ReactiveMongoTemplateIndexTests {
.as(StepVerifier::create) //
.verifyComplete();
Flux.from(factory.getMongoDatabase().runCommand(new org.bson.Document("eval", command))) //
Flux.from(factory.getMongoDatabase().getCollection(template.getCollectionName(Person.class))
.createIndex(new Document("age", -1), new IndexOptions().unique(true).sparse(true))) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
@@ -177,7 +176,7 @@ public class ReactiveMongoTemplateIndexTests {
}
}
assertThat(indexKey).containsEntry("age", -1D);
assertThat(indexKey).containsEntry("age", -1);
assertThat(unique).isTrue();
}).verifyComplete();
@@ -207,12 +206,11 @@ public class ReactiveMongoTemplateIndexTests {
.verifyComplete();
}
@Test // DATAMONGO-1928
@Test // DATAMONGO-1928, DATAMONGO-2264
public void indexCreationShouldFail() throws InterruptedException {
String command = "db.indexfail" + ".createIndex({'field':1}, {'name':'foo', 'unique':true, 'sparse':true}), 1";
Flux.from(factory.getMongoDatabase().runCommand(new org.bson.Document("eval", command))) //
Flux.from(factory.getMongoDatabase().getCollection("indexfail") //
.createIndex(new Document("field", 1), new IndexOptions().name("foo").unique(true).sparse(true)))
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();

View File

@@ -40,7 +40,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId;
@@ -299,16 +298,13 @@ public class ReactiveMongoTemplateUnitTests {
verify(mapReducePublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
@Test // DATAMONGO-1518, DATAMONGO-2264
public void geoNearShouldUseCollationWhenPresent() {
NearQuery query = NearQuery.near(0D, 0D).query(new BasicQuery("{}").collation(Collation.of("fr")));
template.geoNear(query, AutogenerateableId.class).subscribe();
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
verify(aggregatePublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1719

View File

@@ -30,7 +30,6 @@ import org.bson.Document;
import org.bson.codecs.BsonValueCodec;
import org.bson.codecs.configuration.CodecRegistry;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -257,21 +256,12 @@ public class ReactiveSessionBoundMongoTemplateUnitTests {
verify(collection).distinct(eq(clientSession), anyString(), any(), any());
}
@Test // DATAMONGO-1880
@Test // DATAMONGO-1880, DATAMONGO-2264
public void geoNearShouldUseProxiedDatabase() {
template.geoNear(NearQuery.near(new Point(0, 0), Metrics.NEUTRAL), Person.class).subscribe();
verify(database).runCommand(eq(clientSession), any(), eq(Document.class));
}
@Test // DATAMONGO-1880, DATAMONGO-1889
@Ignore("No group by yet - DATAMONGO-1889")
public void groupShouldUseProxiedDatabase() {
// template.group(COLLECTION_NAME, GroupBy.key("firstName"), Person.class).subscribe();
verify(database).runCommand(eq(clientSession), any(), eq(Document.class));
verify(collection).aggregate(eq(clientSession), anyList(), eq(Document.class));
}
@Test // DATAMONGO-1880, DATAMONGO-1890, DATAMONGO-257

View File

@@ -258,14 +258,14 @@ public class SessionBoundMongoTemplateUnitTests {
verify(collection).distinct(eq(clientSession), anyString(), any(), any());
}
@Test // DATAMONGO-1880
@Test // DATAMONGO-1880, DATAMONGO-2264
public void geoNearShouldUseProxiedDatabase() {
when(database.runCommand(any(ClientSession.class), any(), eq(Document.class)))
.thenReturn(new Document("results", Collections.emptyList()));
template.geoNear(NearQuery.near(new Point(0, 0), Metrics.NEUTRAL), Person.class);
verify(database).runCommand(eq(clientSession), any(), eq(Document.class));
verify(collection).aggregate(eq(clientSession), anyList(), eq(Document.class));
}
@Test // DATAMONGO-1880

View File

@@ -52,11 +52,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.CollectionCallback;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.TestEntities;
@@ -779,8 +779,9 @@ public class AggregationTests {
group("make").avg(ConditionalOperators //
.when(Criteria.where("year").gte(2012)) //
.then(1) //
.otherwise(9000)).as("score"),
sort(ASC, "make"));
.otherwise(9000)) //
.as("score"),
sort(ASC, "score"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
@@ -795,7 +796,7 @@ public class AggregationTests {
assertThat(((Number) good.get("score")).longValue()).isEqualTo(9000L);
}
@Test // DATAMONGO-1784
@Test // DATAMONGO-1784, DATAMONGO-2264
public void shouldAllowSumUsingConditionalExpressions() {
mongoTemplate.dropCollection(CarPerson.class);
@@ -823,7 +824,7 @@ public class AggregationTests {
.when(Criteria.where("year").gte(2012)) //
.then(1) //
.otherwise(9000)).as("score"),
sort(ASC, "make"));
sort(ASC, "score"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
@@ -995,23 +996,6 @@ public class AggregationTests {
.isEqualTo((product.netPrice * (1 - product.discountRate) + shippingCosts) * (1 + product.taxRate));
}
@Test
public void shouldThrowExceptionIfUnknownFieldIsReferencedInArithmenticExpressionsInProjection() {
exception.expect(MappingException.class);
exception.expectMessage("unknown");
Product product = new Product("P1", "A", 1.99, 3, 0.05, 0.19);
mongoTemplate.insert(product);
TypedAggregation<Product> agg = newAggregation(Product.class, //
project("name", "netPrice") //
.andExpression("unknown + 1").as("netPricePlus1") //
);
mongoTemplate.aggregate(agg, Document.class);
}
/**
* @see <a href=
* "https://stackoverflow.com/questions/18653574/spring-data-mongodb-aggregation-framework-invalid-reference-in-group-operati">Spring
@@ -1027,18 +1011,19 @@ public class AggregationTests {
unwind("pd"), //
group("pd.pDch") // the nested field expression
.sum("pd.up").as("uplift"), //
project("_id", "uplift"));
project("_id", "uplift"), //
sort(Sort.by("uplift")));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
List<Document> stats = result.getMappedResults();
assertThat(stats.size()).isEqualTo(3);
assertThat(stats.get(0).get("_id").toString()).isEqualTo("C");
assertThat((Integer) stats.get(0).get("uplift")).isEqualTo(2);
assertThat(stats.get(1).get("_id").toString()).isEqualTo("B");
assertThat((Integer) stats.get(1).get("uplift")).isEqualTo(3);
assertThat(stats.get(2).get("_id").toString()).isEqualTo("A");
assertThat((Integer) stats.get(2).get("uplift")).isEqualTo(1);
assertThat(stats.get(0).get("_id").toString()).isEqualTo("A");
assertThat((Integer) stats.get(0).get("uplift")).isEqualTo(1);
assertThat(stats.get(1).get("_id").toString()).isEqualTo("C");
assertThat((Integer) stats.get(1).get("uplift")).isEqualTo(2);
assertThat(stats.get(2).get("_id").toString()).isEqualTo("B");
assertThat((Integer) stats.get(2).get("uplift")).isEqualTo(3);
}
/**
@@ -1151,7 +1136,7 @@ public class AggregationTests {
assertThat((Integer) resultDocument.keySet().size()).isEqualTo(1);
}
@Test // DATAMONGO-788
@Test // DATAMONGO-788, DATAMONGO-2264
public void referencesToGroupIdsShouldBeRenderedProperly() {
mongoTemplate.insert(new DATAMONGO788(1, 1));
@@ -1165,7 +1150,7 @@ public class AggregationTests {
AggregationOperation project = Aggregation.project("xPerY", "x", "y").andExclude("_id");
TypedAggregation<DATAMONGO788> aggregation = Aggregation.newAggregation(DATAMONGO788.class, projectFirst, group,
project);
project, Aggregation.sort(Sort.by("xPerY")));
AggregationResults<Document> aggResults = mongoTemplate.aggregate(aggregation, Document.class);
List<Document> items = aggResults.getMappedResults();
@@ -1347,7 +1332,7 @@ public class AggregationTests {
assertThat(rawResult.containsKey("stages")).isEqualTo(true);
}
@Test // DATAMONGO-954
@Test // DATAMONGO-954, DATAMONGO-2264
@MongoVersion(asOf = "2.6")
public void shouldSupportReturningCurrentAggregationRoot() {
@@ -1359,11 +1344,11 @@ public class AggregationTests {
List<Document> personsWithAge25 = mongoTemplate.find(Query.query(where("age").is(25)), Document.class,
mongoTemplate.getCollectionName(Person.class));
Aggregation agg = newAggregation(group("age").push(Aggregation.ROOT).as("users"));
Aggregation agg = newAggregation(group("age").push(Aggregation.ROOT).as("users"), sort(Sort.by("_id")));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Person.class, Document.class);
assertThat(result.getMappedResults()).hasSize(3);
Document o = result.getMappedResults().get(2);
Document o = result.getMappedResults().get(1);
assertThat(o.get("_id")).isEqualTo((Object) 25);
assertThat((List<?>) o.get("users")).hasSize(2);
@@ -1373,7 +1358,7 @@ public class AggregationTests {
/**
* {@link https://stackoverflow.com/questions/24185987/using-root-inside-spring-data-mongodb-for-retrieving-whole-document}
*/
@Test // DATAMONGO-954
@Test // DATAMONGO-954, DATAMONGO-2264
@MongoVersion(asOf = "2.6")
public void shouldSupportReturningCurrentAggregationRootInReference() {
@@ -1771,7 +1756,7 @@ public class AggregationTests {
new Document("_id", "2").append("finalTotal", 10.25D));
}
@Test // DATAMONGO-1551
@Test // DATAMONGO-1551, DATAMONGO-2264
@MongoVersion(asOf = "3.4")
public void graphLookupShouldBeAppliedCorrectly() {
@@ -1789,16 +1774,18 @@ public class AggregationTests {
.connectTo("name") //
.depthField("depth") //
.maxDepth(5) //
.as("reportingHierarchy"));
.as("reportingHierarchy"), //
project("id", "depth", "name", "reportsTo", "reportingHierarchy"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
Document object = result.getUniqueMappedResult();
List<Object> list = (List<Object>) object.get("reportingHierarchy");
Assert.assertThat(object, isBsonObject().containing("reportingHierarchy", List.class));
assertThat((Document) list.get(0)).containsEntry("name", "Dev").containsEntry("depth", 1L);
assertThat((Document) list.get(1)).containsEntry("name", "Eliot").containsEntry("depth", 0L);
assertThat(object).containsEntry("name", "Andrew").containsEntry("reportsTo", "Eliot");
assertThat(list).containsOnly(
new Document("_id", 2).append("name", "Eliot").append("reportsTo", "Dev").append("depth", 0L).append("_class", Employee.class.getName()),
new Document("_id", 1).append("name", "Dev").append("depth", 1L).append("_class", Employee.class.getName()));
}
@Test // DATAMONGO-1552

View File

@@ -17,10 +17,23 @@ package org.springframework.data.mongodb.core.aggregation;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Distance;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
/**
* Unit tests for {@link GeoNearOperation}.
@@ -53,4 +66,201 @@ public class GeoNearOperationUnitTests {
assertThat(DocumentTestUtils.getAsDocument(document, "$geoNear")).containsEntry("key", "geo-index-1");
}
@Test // DATAMONGO-2264
public void rendersMaxDistanceCorrectly() {
NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(new Distance(30.0));
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).maxDistance(30.0).doc());
}
@Test // DATAMONGO-2264
public void rendersMinDistanceCorrectly() {
NearQuery query = NearQuery.near(10.0, 20.0).minDistance(new Distance(30.0));
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).minDistance(30.0).doc());
}
@Test // DATAMONGO-2264
public void rendersSphericalCorrectly() {
NearQuery query = NearQuery.near(10.0, 20.0).spherical(true);
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).spherical(true).doc());
}
@Test // DATAMONGO-2264
public void rendersDistanceMultiplier() {
NearQuery query = NearQuery.near(10.0, 20.0).inKilometers();
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).spherical(true).distanceMultiplier(6378.137).doc());
}
@Test // DATAMONGO-2264
public void rendersIndexKey() {
NearQuery query = NearQuery.near(10.0, 20.0);
assertThat(new GeoNearOperation(query, "distance").useIndex("index-1").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).key("index-1").doc());
}
@Test // DATAMONGO-2264
public void rendersQuery() {
NearQuery query = NearQuery.near(10.0, 20.0).query(Query.query(Criteria.where("city").is("Austin")));
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).query(new Document("city", "Austin")).doc());
}
@Test // DATAMONGO-2264
public void rendersMappedQuery() {
NearQuery query = NearQuery.near(10.0, 20.0).query(Query.query(Criteria.where("city").is("Austin")));
assertThat(
new GeoNearOperation(query, "distance").toPipelineStages(typedAggregationOperationContext(GeoDocument.class)))
.containsExactly($geoNear().near(10.0, 20.0).query(new Document("ci-ty", "Austin")).doc());
}
@Test // DATAMONGO-2264
public void appliesSkipFromNearQuery() {
NearQuery query = NearQuery.near(10.0, 20.0).skip(10L);
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).doc(), new Document("$skip", 10L));
}
@Test // DATAMONGO-2264
public void appliesLimitFromNearQuery() {
NearQuery query = NearQuery.near(10.0, 20.0).limit(10L);
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).doc(), new Document("$limit", 10L));
}
@Test // DATAMONGO-2264
public void appliesSkipAndLimitInOrder() {
NearQuery query = NearQuery.near(10.0, 20.0).limit(10L).skip(3L);
assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT))
.containsExactly($geoNear().near(10.0, 20.0).doc(), new Document("$skip", 3L), new Document("$limit", 10L));
}
private TypeBasedAggregationOperationContext typedAggregationOperationContext(Class<?> type) {
MongoMappingContext mappingContext = new MongoMappingContext();
MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
return new TypeBasedAggregationOperationContext(type, mappingContext, new QueryMapper(converter));
}
GeoNearDocumentBuilder $geoNear() {
return new GeoNearDocumentBuilder();
}
static class GeoDocument {
@Id String id;
@Field("ci-ty") String city;
}
static class GeoNearDocumentBuilder {
Document target = new Document("distanceField", "distance").append("distanceMultiplier", 1.0D).append("spherical",
false);
GeoNearDocumentBuilder maxDistance(@Nullable Number value) {
if (value != null) {
target.put("maxDistance", value);
} else {
target.remove("maxDistance");
}
return this;
}
GeoNearDocumentBuilder minDistance(@Nullable Number value) {
if (value != null) {
target.put("minDistance", value);
} else {
target.remove("minDistance");
}
return this;
}
GeoNearDocumentBuilder near(Number... coordinates) {
target.put("near", Arrays.asList(coordinates));
return this;
}
GeoNearDocumentBuilder spherical(@Nullable Boolean value) {
if (value != null) {
target.put("spherical", value);
} else {
target.remove("spherical");
}
return this;
}
GeoNearDocumentBuilder distanceField(@Nullable String value) {
if (value != null) {
target.put("distanceField", value);
} else {
target.remove("distanceField");
}
return this;
}
GeoNearDocumentBuilder distanceMultiplier(Number value) {
if (value != null) {
target.put("distanceMultiplier", value);
} else {
target.remove("distanceMultiplier");
}
return this;
}
GeoNearDocumentBuilder key(String value) {
if (value != null) {
target.put("key", value);
} else {
target.remove("key");
}
return this;
}
GeoNearDocumentBuilder query(Document value) {
if (value != null) {
target.put("query", value);
} else {
target.remove("query");
}
return this;
}
Document doc() {
return new Document("$geoNear", new Document(target));
}
}
// TODO: we need to test this to the full extend
}

View File

@@ -24,11 +24,11 @@ import lombok.Data;
import java.util.Arrays;
import java.util.List;
import org.assertj.core.data.Percentage;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
@@ -95,7 +95,7 @@ public class GeoJsonTests {
removeCollections();
}
@Test // DATAMONGO-1135
@Test // DATAMONGO-1135, DATAMONGO-2264
public void geoNear() {
NearQuery geoNear = NearQuery.near(new GeoJsonPoint(-73, 40), Metrics.KILOMETERS).num(10).maxDistance(150);
@@ -104,6 +104,42 @@ public class GeoJsonTests {
assertThat(result.getContent()).isNotEmpty();
assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
assertThat(result.getAverageDistance().getValue()).isCloseTo(117.84629457941556, Percentage.withPercentage(0.001));
}
@Test // DATAMONGO-2264
public void geoNearShouldNotOverridePropertyWithDefaultNameForCalculatedDistance/* namely "dis" */() {
NearQuery geoNear = NearQuery.near(new GeoJsonPoint(-73, 40), Metrics.KILOMETERS).num(10).maxDistance(150);
GeoResults<VenueWithDistanceField> result = template.geoNear(geoNear, VenueWithDistanceField.class);
assertThat(result.getContent()).isNotEmpty();
assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
assertThat(result.getAverageDistance().getValue()).isCloseTo(117.84629457941556, Percentage.withPercentage(0.001));
result.getContent().forEach(it -> {
assertThat(it.getDistance().getValue()).isNotZero();
assertThat(it.getContent().getDis()).isNull();
});
}
@Test // DATAMONGO-2264
public void geoNearShouldAllowToReadBackCalculatedDistanceIntoTargetTypeProperty/* namely "dis" */() {
NearQuery geoNear = NearQuery.near(new GeoJsonPoint(-73, 40), Metrics.KILOMETERS).num(10).maxDistance(150);
GeoResults<VenueWithDistanceField> result = template.geoNear(geoNear, Venue2DSphere.class,
template.getCollectionName(Venue2DSphere.class), VenueWithDistanceField.class);
assertThat(result.getContent()).isNotEmpty();
assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
assertThat(result.getAverageDistance().getValue()).isCloseTo(117.84629457941556, Percentage.withPercentage(0.001));
result.getContent().forEach(it -> {
assertThat(it.getDistance().getValue()).isNotZero();
assertThat(it.getContent().getDis()).isEqualTo(it.getDistance().getValue());
});
}
@Test // DATAMONGO-1148
@@ -124,8 +160,7 @@ public class GeoJsonTests {
@Test // DATAMONGO-1348
public void geoNearShouldReturnDistanceCorrectly/*which is using the meters*/() {
NearQuery geoNear = NearQuery.near(new Point(-73.99171, 40.738868), Metrics.KILOMETERS).num(10)
.maxDistance(0.4);
NearQuery geoNear = NearQuery.near(new Point(-73.99171, 40.738868), Metrics.KILOMETERS).num(10).maxDistance(0.4);
GeoResults<Venue2DSphere> result = template.geoNear(geoNear, Venue2DSphere.class);
@@ -469,6 +504,23 @@ public class GeoJsonTests {
}
}
static class VenueWithDistanceField extends Venue2DSphere {
private Double dis; // geoNear command default distance field name
public VenueWithDistanceField(String name, double[] location) {
super(name, location);
}
public Double getDis() {
return dis;
}
public void setDis(Double dis) {
this.dis = dis;
}
}
static class DocumentWithPropertyUsingGeoJsonType {
String id;

View File

@@ -24,10 +24,13 @@ import org.bson.Document;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -44,6 +47,8 @@ import com.mongodb.client.MongoCollection;
@ContextConfiguration("classpath:infrastructure.xml")
public class GroupByTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_MOST_4_0 = MongoVersionRule.atMost(Version.parse("4.0.999"));
@Autowired MongoTemplate mongoTemplate;
@Before

View File

@@ -89,15 +89,14 @@ public class NearQueryUnitTests {
assertThat(query.getMetric(), is((Metric) Metrics.MILES));
}
@Test // DATAMONGO-445
@Test // DATAMONGO-445, DATAMONGO-2264
public void shouldTakeSkipAndLimitSettingsFromGivenPageable() {
Pageable pageable = PageRequest.of(3, 5);
NearQuery query = NearQuery.near(new Point(1, 1)).with(pageable);
assertThat(query.getSkip(), is((long) pageable.getPageNumber() * pageable.getPageSize()));
assertThat((Long) query.toDocument().get("num"),
is((long) (pageable.getPageNumber() + 1) * pageable.getPageSize()));
assertThat(query.toDocument().get("num"), is((long) pageable.getPageSize()));
}
@Test // DATAMONGO-445
@@ -112,7 +111,7 @@ public class NearQueryUnitTests {
assertThat((Long) query.toDocument().get("num"), is((long) limit));
}
@Test // DATAMONGO-445
@Test // DATAMONGO-445, DATAMONGO-2264
public void shouldTakeSkipAndLimitSettingsFromPageableEvenIfItWasSpecifiedOnQuery() {
int limit = 10;
@@ -122,8 +121,7 @@ public class NearQueryUnitTests {
.query(Query.query(Criteria.where("foo").is("bar")).limit(limit).skip(skip)).with(pageable);
assertThat(query.getSkip(), is((long) pageable.getPageNumber() * pageable.getPageSize()));
assertThat((Long) query.toDocument().get("num"),
is((long) (pageable.getPageNumber() + 1) * pageable.getPageSize()));
assertThat(query.toDocument().get("num"), is((long) pageable.getPageSize()));
}
@Test // DATAMONGO-829

View File

@@ -25,6 +25,7 @@ import java.util.Optional;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -32,6 +33,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.data.mongodb.test.util.MongoVersion;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -46,6 +49,8 @@ import com.mongodb.MongoClient;
@ContextConfiguration
public class ComplexIdRepositoryIntegrationTests {
public @Rule MongoVersionRule mongoVersionRule = MongoVersionRule.any();
@Configuration
@EnableMongoRepositories
static class Config extends AbstractMongoConfiguration {
@@ -129,6 +134,7 @@ public class ComplexIdRepositoryIntegrationTests {
}
@Test // DATAMONGO-1373
@MongoVersion(until = "4.0.999")
public void composedAnnotationFindMetaShouldWorkWhenUsingComplexId() {
repo.save(userWithId);

View File

@@ -15,12 +15,14 @@
*/
package org.springframework.data.mongodb.repository.support;
import org.springframework.data.mongodb.core.query.BasicQuery;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.LinkedHashSet;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -100,19 +102,20 @@ public class ReactiveQuerydslMongoPredicateExecutorTests {
MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, operations);
operations.dropCollection(Person.class) //
.as(StepVerifier::create) //
.verifyComplete();
dave = new Person("Dave", "Matthews", 42);
oliver = new Person("Oliver August", "Matthews", 4);
carter = new Person("Carter", "Beauford", 49);
person = new QPerson("person");
operations.insertAll(Arrays.asList(oliver, dave, carter)).as(StepVerifier::create) //
.expectNextCount(3) //
.verifyComplete();
Flux.merge(operations.insert(oliver), operations.insert(dave), operations.insert(carter)).then() //
.as(StepVerifier::create).verifyComplete();
}
@After
public void tearDown() {
operations.remove(new BasicQuery("{}"), "person").then().as(StepVerifier::create).verifyComplete();
operations.remove(new BasicQuery("{}"), "uer").then().as(StepVerifier::create).verifyComplete();
}
@Test // DATAMONGO-2182
@@ -200,10 +203,10 @@ public class ReactiveQuerydslMongoPredicateExecutorTests {
User user3 = new User();
user3.setUsername("user-3");
operations.insertAll(Arrays.asList(user1, user2, user3)) //
Flux.merge(operations.save(user1), operations.save(user2), operations.save(user3)) //
.then() //
.as(StepVerifier::create) //
.expectNextCount(3) //
.verifyComplete();
.verifyComplete(); //
Person person1 = new Person("Max", "The Mighty");
person1.setCoworker(user1);

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.test.util;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import org.bson.Document;
import com.mongodb.MongoClientURI;
@@ -78,8 +80,9 @@ public class MongoTestUtils {
com.mongodb.reactivestreams.client.MongoDatabase database = client.getDatabase(dbName)
.withWriteConcern(WriteConcern.MAJORITY).withReadPreference(ReadPreference.primary());
return Mono.from(database.getCollection(collectionName).drop())
.then(Mono.from(database.createCollection(collectionName)));
return Mono.from(database.getCollection(collectionName).drop()) //
.then(Mono.from(database.createCollection(collectionName))) //
.delayElement(Duration.ofMillis(10)); // server replication time
}
/**

View File

@@ -3,6 +3,7 @@
[[new-features.2-2-0]]
== What's New in Spring Data MongoDB 2.2
* Compatible with MongoDB 4.2.
* <<mongo.query.kotlin-support,Type-safe Queries for Kotlin>>
* <<mongodb.reactive.repositories.queries.type-safe,Querydsl support for reactive repositories>> via `ReactiveQuerydslPredicateExecutor`.
* <<reactive.gridfs,Reactive GridFS support>>.

View File

@@ -1333,6 +1333,33 @@ List<Venue> venues =
[[mongo.geo-near]]
==== Geo-near Queries
[WARNING]
====
*Changed in 2.2!* +
https://docs.mongodb.com/master/release-notes/4.2-compatibility/[MongoDB 4.2] removed support for the
`geoNear` command which had been previously used to run the `NearQuery`.
Spring Data MongoDB 2.2 `MongoOperations#geoNear` uses the `$geoNear` https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/[aggregation]
instead of the `geoNear` command to run a `NearQuery`.
The calculated distance (the `dis` when using a geoNear command) previously returned within a wrapper type now is embedded
into the resulting document. If the given domain type already contains a property with equal name, the calculated distance
is named `calculated-distance` or if that name is taken as well, postfixed with a random hash.
Target types may contain a property named after the returned distance to (additionally) read it back directly into the domain
type as shown below.
[source,java]
----
GeoResults<VenueWithDisField> = template.query(Venue.class) <1>
.as(VenueWithDisField.class) <2>
.near(NearQuery.near(new GeoJsonPoint(-73.99, 40.73), KILOMETERS))
.all();
----
<1> Domain type used to identify the target collection and potential query mapping.
<2> Target type containing a `dis` field of type `Number`.
====
MongoDB supports querying the database for geo locations and calculating the distance from a given origin at the same time. With geo-near queries, you can express queries such as "find all restaurants in the surrounding 10 miles". To let you do so, `MongoOperations` provides `geoNear(…)` methods that take a `NearQuery` as an argument (as well as the already familiar entity type and collection), as shown in the following example:
[source,java]
@@ -2226,6 +2253,13 @@ Note that you can specify additional limit and sort values on the query, but you
[[mongo.server-side-scripts]]
== Script Operations
[WARNING]
====
https://docs.mongodb.com/master/release-notes/4.2-compatibility/[MongoDB 4.2] removed support for the `eval` command used
by `ScriptOperations`. +
There is no replacement for the removed functionallity.
====
MongoDB allows executing JavaScript functions on the server by either directly sending the script or calling a stored one. `ScriptOperations` can be accessed through `MongoTemplate` and provides basic abstraction for `JavaScript` usage. The following example shows how to us the `ScriptOperations` class:
====