diff --git a/.travis.yml b/.travis.yml index 604395caa..a6382b721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java index 5d992b4a4..71d59993b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java @@ -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"; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index cc2cfe88f..24078c973 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -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 { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 11b44ee67..82a7badee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -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.
+ * Please use {@link #aggregate(TypedAggregation, String, Class) } with a + * {@link org.springframework.data.mongodb.core.aggregation.GroupOperation} instead. */ + @Deprecated GroupByResults group(String inputCollectionName, GroupBy groupBy, Class 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.
+ * 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 GroupByResults group(@Nullable Criteria criteria, String inputCollectionName, GroupBy groupBy, Class 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 - * Spring's Type - * Conversion" for more details. + * Spring's + * Type Conversion" for more details. *

*

* 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 - * Spring's Type - * Conversion" for more details. + * Spring's + * Type Conversion" for more details. * * @param objectToSave the object to store in the collection. Must not be {@literal null}. * @return the saved object. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 3beda20b4..389e9f48e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -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 GeoResults geoNear(NearQuery near, Class domainType, String collectionName, Class 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 results = aggregate($geoNear, collection, Document.class); + + DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, + new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collection), near.getMetric()); + + List> result = new ArrayList<>(); + + BigDecimal aggregate = BigDecimal.ZERO; + for (Document element : results) { + + GeoResult 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 results = (List) commandResult.get("results"); - results = results == null ? Collections.emptyList() : results; - - DocumentCallback> callback = new GeoNearResultDocumentCallback<>( - new ProjectingReadCallback<>(mongoConverter, domainType, returnType, collectionName), near.getMetric()); - List> 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 MongoDB Jira: SERVER-3925 - */ - 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 implements DocumentCallback> { + private final String distanceField; private final DocumentCallback 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 delegate, Metric metric) { + GeoNearResultDocumentCallback(String distanceField, DocumentCallback 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 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)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 328e92e30..103ca5f49 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -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 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 callback = new GeoNearResultDocumentCallback<>( - new ProjectingReadCallback<>(mongoConverter, entityClass, returnType, collectionName), near.getMetric()); - - return executeCommand(command, this.readPreference).flatMapMany(document -> { - - List 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 implements DocumentCallback> { + private final String distanceField; private final DocumentCallback 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 delegate, Metric metric) { + GeoNearResultDocumentCallback(String distanceField, DocumentCallback delegate, Metric metric) { Assert.notNull(delegate, "DocumentCallback must not be null!"); + this.distanceField = distanceField; this.delegate = delegate; this.metric = metric; } public GeoResult 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)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java index 9e2aaab84..0dafcee5b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java @@ -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 { /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index dead6dae9..5cf5c852c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -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}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java index 4439d9023..932fbaf6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java @@ -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 toPipelineStages(AggregationOperationContext context) { + return Collections.singletonList(toDocument(context)); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java index 827496aa3..b77200682 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java @@ -52,7 +52,7 @@ class AggregationOperationRenderer { for (AggregationOperation operation : operations) { - operationDocuments.add(operation.toDocument(contextToUse)); + operationDocuments.addAll(operation.toPipelineStages(contextToUse)); if (operation instanceof FieldsExposingAggregationOperation) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java index cc3e574a1..6295cd45e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java @@ -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 toPipelineStages(AggregationOperationContext context) { + + Document command = toDocument(context); + Number limit = (Number) command.get("$geoNear", Document.class).remove("num"); + + List 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; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java index 0e49dfff9..b5fa88c6d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java @@ -128,7 +128,13 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation */ @Override public ExposedFields getFields() { - return ExposedFields.from(new ExposedField(as, true)); + + List 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])); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index 51b58dca2..dc4207d82 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -79,8 +79,6 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio */ @Override public FieldReference getReference(Field field) { - - PropertyPath.from(field.getTarget(), type); return getReferenceFor(field); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java index c0046de0f..0520a14ea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java @@ -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; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java index 8085c609c..e81a44f42 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupByResults.java @@ -29,7 +29,9 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @author Mark Paluch * @param 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 implements Iterable { private final List mappedResults; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java index 3740a7f05..1a5a73cf3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java @@ -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) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index 318550c73..4a9a309c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -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()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java index b762ab09f..a0ecfe19b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java @@ -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; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/NamedMongoScript.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/NamedMongoScript.java index 2a844b9cc..d02dd7966 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/NamedMongoScript.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/NamedMongoScript.java @@ -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; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java index 556311f4b..86c005c63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java @@ -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); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Meta.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Meta.java index dde664b16..704cf9aac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Meta.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Meta.java @@ -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; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java index 62753898d..8a33392bc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java @@ -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 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)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java index f7b8490cc..b6af070e6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java @@ -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 { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index c067e90bc..f4e8f4cff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -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 indexInfo = template.getCollection(template.getCollectionName(Person.class)) .listIndexes(); @@ -443,7 +444,7 @@ public class MongoTemplateTests { } } - assertThat(indexKey, IsMapContaining. hasEntry("age", -1D)); + assertThat(indexKey, IsMapContaining. hasEntry("age", -1)); assertThat(unique, is(true)); IndexInfo info = template.indexOps(Person.class).getIndexInfo().get(1); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index f99db1bab..edddcd53d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -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 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 capture = ArgumentCaptor.forClass(Document.class); - verify(this.db, times(1)).runCommand(capture.capture(), any(Class.class)); + ArgumentCaptor> 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 capture = ArgumentCaptor.forClass(Document.class); - verify(this.db, times(1)).runCommand(capture.capture(), any(Class.class)); + ArgumentCaptor> 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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java index 8b7ed9fd8..e7cebb108 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateIndexTests.java @@ -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(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 0acb22832..a015c1db8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -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 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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 9f4d03308..b0233611f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index 38ac9bd40..ef8b23dc6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 37056d788..7e481e511 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -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 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 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 agg = newAggregation(Product.class, // - project("name", "netPrice") // - .andExpression("unknown + 1").as("netPricePlus1") // - ); - - mongoTemplate.aggregate(agg, Document.class); - } - /** * @see 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 result = mongoTemplate.aggregate(agg, Document.class); List 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 aggregation = Aggregation.newAggregation(DATAMONGO788.class, projectFirst, group, - project); + project, Aggregation.sort(Sort.by("xPerY"))); AggregationResults aggResults = mongoTemplate.aggregate(aggregation, Document.class); List 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 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 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 result = mongoTemplate.aggregate(agg, Document.class); Document object = result.getUniqueMappedResult(); List list = (List) 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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index 08e7a4760..094b3279c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -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 } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java index a31fdf450..b2e780fc2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java @@ -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 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 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 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; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java index 462be4768..038a3245e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/GroupByTests.java @@ -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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java index e45f858cc..49b8f2b48 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java @@ -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 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ComplexIdRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ComplexIdRepositoryIntegrationTests.java index 8b92abd59..135cc7e17 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ComplexIdRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ComplexIdRepositoryIntegrationTests.java @@ -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); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java index c619982a6..47ca7c549 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java @@ -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 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); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java index f489b7402..9c28be7e8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestUtils.java @@ -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 } /** diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 8050d212f..c4adf5561 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -3,6 +3,7 @@ [[new-features.2-2-0]] == What's New in Spring Data MongoDB 2.2 +* Compatible with MongoDB 4.2. * <> * <> via `ReactiveQuerydslPredicateExecutor`. * <>. diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index c168d27f6..d6f94af34 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1333,6 +1333,33 @@ List 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 = 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: ====