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 8f199e8ff..ad0f74ad1 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 @@ -19,8 +19,6 @@ import java.util.Collection; import java.util.List; import java.util.Set; -import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; -import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.geo.GeoResult; import org.springframework.data.mongodb.core.geo.GeoResults; @@ -316,6 +314,19 @@ public interface MongoOperations { */ AggregationResults aggregate(String inputCollectionName, AggregationPipeline pipeline, Class entityClass); + /** + * Execute an aggregation operation. The raw results will be mapped to the given entity class. + * + * @param inputCollectionName the collection there the aggregation operation will read from, must not be + * {@literal null} or empty. + * @param entityClass The parameterized type of the returned list, must not be {@literal null}. + * @param operations The aggregation operations, must not be {@literal null}. + * @return The results of the aggregation operation. + * @since 1.3 + */ + AggregationResults aggregate(String inputCollectionName, Class entityClass, + AggregationOperation... operations); + /** * Execute a map-reduce operation. The map-reduce operation will be formed with an output type of INLINE * 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 7fed406f1..45cf3ff80 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 @@ -53,6 +53,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.aggregation.operation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -1238,6 +1239,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { return new AggregationResults(mappedResults, commandResult); } + public AggregationResults aggregate(String inputCollectionName, Class entityClass, AggregationOperation... operations) { + return aggregate(inputCollectionName, new AggregationPipeline(operations), entityClass); + } + + protected String replaceWithResourceIfNecessary(String function) { String func = function; 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 new file mode 100644 index 000000000..bff5cd6f8 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperation.java @@ -0,0 +1,19 @@ +package org.springframework.data.mongodb.core.aggregation.operation; + +import com.mongodb.DBObject; + +/** + * Represents one single operation in an aggregation pipeline + * + * @author Sebastian Herold + * @since 1.3 + */ +public interface AggregationOperation { + + /** + * Gets the {@link DBObject} behind this operation + * + * @return the DBObject + */ + DBObject getDBObject(); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java index 81e229fae..492096792 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.aggregation.operation.AggregationOperation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.util.Assert; @@ -39,6 +40,18 @@ public class AggregationPipeline { private final List operations = new ArrayList(); + public AggregationPipeline() { + } + + public AggregationPipeline(AggregationOperation... operations) { + Assert.notNull(operations, "Operations are missing"); + + for (AggregationOperation operation : operations) { + Assert.notNull(operation, "Operation is not allowed to be null"); + this.operations.add(operation.getDBObject()); + } + } + /** * Adds a projection operation to the pipeline. * @@ -81,7 +94,7 @@ public class AggregationPipeline { /** * Adds a group operation to the pipeline. * - * @param projection JSON string holding the group, must not be {@literal null} or empty. + * @param group JSON string holding the group, must not be {@literal null} or empty. * @return The pipeline. */ public AggregationPipeline group(String group) { @@ -118,7 +131,7 @@ public class AggregationPipeline { /** * Adds a match operation to the pipeline that is basically a query on the collections. * - * @param projection JSON string holding the criteria, must not be {@literal null} or empty. + * @param match JSON string holding the criteria, must not be {@literal null} or empty. * @return The pipeline. */ public AggregationPipeline match(String match) { @@ -161,6 +174,14 @@ public class AggregationPipeline { return operations; } + /** + * creates an empty pipeline + * @return the new pipeline + */ + public static AggregationPipeline pipeline() { + return new AggregationPipeline(); + } + private AggregationPipeline addDocumentOperation(String opName, String operation) { Assert.hasText(operation, "Missing operation name!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java new file mode 100644 index 000000000..739594a2f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -0,0 +1,334 @@ +package org.springframework.data.mongodb.core.aggregation.operation; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import org.springframework.data.mongodb.core.aggregation.ReferenceUtil; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Encapsulates the aggregation framework + * + * $group-operation + * + * + * @author Sebastian Herold + * @since 1.3 + */ +public class GroupOperation implements AggregationOperation { + + private static final String ID_KEY = "_id"; + private final Object id; + private final Map fields = new HashMap(); + + public GroupOperation(Object id) { + this.id = id; + } + + public DBObject getDBObject() { + DBObject projection = new BasicDBObject(ID_KEY, id); + for (Entry entry : fields.entrySet()) { + projection.put(entry.getKey(), entry.getValue()); + } + return new BasicDBObject("$group", projection); + } + + public GroupOperation addField(String key, DBObject value) { + Assert.hasText(key, "Key is empty"); + Assert.notNull(value, "Value is null"); + + String trimmedKey = key.trim(); + if (ID_KEY.equals(trimmedKey)) { + throw new IllegalArgumentException("_id field can only be set in constructor"); + } + + fields.put(key, value); + + return this; + } + + /** + * Adds a field with the + * $addToSet operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$addToSet: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation addToSet(String name, String field) { + return addOperation("$addToSet", name, field); + } + + /** + * Adds a field with the + * $first operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$first: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation first(String name, String field) { + return addOperation("$first", name, field); + } + + /** + * Adds a field with the + * $last operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$last: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation last(String name, String field) { + return addOperation("$last", name, field); + } + + /** + * Adds a field with the + * $max operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$max: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation max(String name, String field) { + return addOperation("$max", name, field); + } + + /** + * Adds a field with the + * $min operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$min: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation min(String name, String field) { + return addOperation("$min", name, field); + } + + + /** + * Adds a field with the + * $avg operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$avg: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation avg(String name, String field) { + return addOperation("$avg", name, field); + } + + + /** + * Adds a field with the + * $push operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$push: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation push(String name, String field) { + return addOperation("$push", name, field); + } + + /** + * Adds a field with the + * $sum operation + * with a constant value. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$sum: increment}
+     *     }}
+     * 
+ * @param name key of the field + * @param increment increment for each item + * @return + * + */ + public GroupOperation count(String name, double increment) { + return addField(name, new BasicDBObject("$sum", increment)); + } + + /** + * Adds a field with the + * $sum operation + * count every item. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$sum: 1}
+     *     }}
+     * 
+ * @param name key of the field + * @return + * + */ + public GroupOperation count(String name) { + return count(name, 1); + } + + /** + * Adds a field with the + * $sum operation. + *
+     *     {$group: {
+     *          _id: "$id_field",
+     *          name: {$sum: "$field"}
+     *     }}
+     * 
+ * @param name key of the field + * @param field reference to a field of the document + * @return + * + */ + public GroupOperation sum(String name, String field) { + return addOperation("$sum", name, field); + } + + /** + * Creates a $group operation with _id referencing to a field of the document. The + * returned db object equals to
{_id: "$field"}
+ * @param field + * @return + */ + public static GroupOperation group(String field) { + return new GroupOperation(ReferenceUtil.safeReference(field)); + } + + protected GroupOperation addOperation(String operation, String name, String field) { + return addField(name, new BasicDBObject(operation, ReferenceUtil.safeReference(field))); + } + + /** + * Creates a $group operation with a id that consists of multiple fields. + * + * Using {@link IdField#idField(String)} or {@link IdField#idField(String, String)} you can easily create + * complex id fields like: + *
+     *
+     *     group(idField("path"), idField("pageView", "page.views"), idField("field3"))
+     *
+     * 
+ * which would result in: + *
+     *
+     *     {$group: {_id: {path: "$path", pageView: "$page.views", field3: "$field3"}}}
+     *
+     * 
+ * @param idFields + * @return + */ + public static GroupOperation group(IdField... idFields) { + Assert.notNull(idFields, "Combined id is null"); + + BasicDBObject id = new BasicDBObject(); + for (IdField idField : idFields) { + id.put(idField.getKey(), idField.getValue()); + } + + return new GroupOperation(id); + } + + /** + * Represents a single field in a complex id of a $group operation. + * + * For example: + *
+     *     {$group: {_id: {key: "$value"}}}
+     * 
+ */ + public static class IdField { + + private final String key; + private final String value; + + public IdField(String key, String value) { + Assert.hasText(key, "Key is empty"); + Assert.hasText(value, "Value is empty"); + + this.key = ReferenceUtil.safeNonReference(key); + this.value = ReferenceUtil.safeReference(value); + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + /** + * Creates an id field with the name of the referenced field: + *
+         *     _id: {field: "$field"}
+         * 
+ * @param field reference to a field of the document + * @return the id field + */ + public static IdField idField(String field) { + return new IdField(field, field); + } + + /** + * Creates an id field with key and reference + *
+         *     _id: {key: "$field"}
+         * 
+ * @param key the key + * @param field reference to a field of the document + * @return the id field + */ + public static IdField idField(String key, String field) { + return new IdField(key, field); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java new file mode 100644 index 000000000..1c4bb0f1d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java @@ -0,0 +1,28 @@ +package org.springframework.data.mongodb.core.aggregation.operation; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import org.springframework.data.mongodb.core.query.Criteria; + +/** + * Encapsulates the $match-operation + * + * @author Sebastian Herold + * @since 1.3 + */ +public class MatchOperation implements AggregationOperation { + + private final DBObject criteria; + + public MatchOperation(Criteria criteria) { + this.criteria = criteria.getCriteriaObject(); + } + + public DBObject getDBObject() { + return new BasicDBObject("$match", criteria); + } + + public static MatchOperation match(Criteria criteria) { + return new MatchOperation(criteria); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Projection.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Projection.java index c4ef22891..a1fcc08dd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Projection.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Projection.java @@ -39,9 +39,7 @@ import com.mongodb.DBObject; */ public class Projection { - private static final String REFERENCE_PREFIX = "$"; - - /** Stack of key names. Size is 0 or 1. */ + /** Stack of key names. Size is 0 or 1. */ private final Stack reference = new Stack(); private final DBObject document = new BasicDBObject(); @@ -105,7 +103,7 @@ public class Projection { Assert.hasText(key, "Missing key"); try { - document.put(key, rightHandSide(safeReference(reference.pop()))); + document.put(key, rightHandSide(ReferenceUtil.safeReference(reference.pop()))); } catch (EmptyStackException e) { throw new InvalidDataAccessApiUsageException("Invalid use of as()", e); } @@ -125,7 +123,7 @@ public class Projection { Assert.notNull(n, "Missing number"); - rightHandExpression = createArrayObject(op, safeReference(reference.peek()), n); + rightHandExpression = createArrayObject(op, ReferenceUtil.safeReference(reference.peek()), n); return this; } @@ -134,7 +132,7 @@ public class Projection { List list = new ArrayList(); Collections.addAll(list, items); - return new BasicDBObject(safeReference(op), list); + return new BasicDBObject(ReferenceUtil.safeReference(op), list); } private void safePop() { @@ -144,18 +142,7 @@ public class Projection { } } - private String safeReference(String key) { - - Assert.hasText(key); - - if (!key.startsWith(REFERENCE_PREFIX)) { - return REFERENCE_PREFIX + key; - } else { - return key; - } - } - - private Object rightHandSide(Object defaultValue) { + private Object rightHandSide(Object defaultValue) { Object value = rightHandExpression != null ? rightHandExpression : defaultValue; rightHandExpression = null; return value; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java new file mode 100644 index 000000000..ac65b5379 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReferenceUtil.java @@ -0,0 +1,45 @@ +package org.springframework.data.mongodb.core.aggregation; + +import org.springframework.util.Assert; + +/** + * Utility class for mongo db reference operator $ + */ +public class ReferenceUtil { + + public static final String REFERENCE_PREFIX = "$"; + + /** + * Ensures that the returned string begins with {@link #REFERENCE_PREFIX $} + * + * @param key reference key with or without {@link #REFERENCE_PREFIX $} at the beginning + * @return key that definitely begins with {@link #REFERENCE_PREFIX $} + */ + public static String safeReference(String key) { + + Assert.hasText(key); + + if (!key.startsWith(REFERENCE_PREFIX)) { + return REFERENCE_PREFIX + key; + } else { + return key; + } + } + + /** + * Ensures that the returned string does not start with {@link #REFERENCE_PREFIX $} + * + * @param field reference key with or without {@link #REFERENCE_PREFIX $} at the beginning + * @return key that definitely does not begin with {@link #REFERENCE_PREFIX $} + */ + public static String safeNonReference(String field) { + + Assert.hasText(field); + + if (field.startsWith(REFERENCE_PREFIX)) { + return field.substring(REFERENCE_PREFIX.length()); + } + + return field; + } +}