DATAMONGO-586 - Further enhancements to Aggregation API.

Fixed parameter names in comments. Add static factory method. Implement basic aggregation operation join point. Implement match operation. Extracted ReferenceUtil. Created starting point of $group operation with _id field definition and $addToSet fields.
This commit is contained in:
Sebastian Herold
2013-04-23 07:43:57 +02:00
committed by Oliver Gierke
parent 4d65aa7207
commit b7b61405f9
8 changed files with 473 additions and 22 deletions

View File

@@ -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 {
*/
<T> AggregationResults<T> aggregate(String inputCollectionName, AggregationPipeline pipeline, Class<T> 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
*/
<T> AggregationResults<T> aggregate(String inputCollectionName, Class<T> entityClass,
AggregationOperation... operations);
/**
* Execute a map-reduce operation. The map-reduce operation will be formed with an output type of INLINE
*

View File

@@ -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<T>(mappedResults, commandResult);
}
public <T> AggregationResults<T> aggregate(String inputCollectionName, Class<T> entityClass, AggregationOperation... operations) {
return aggregate(inputCollectionName, new AggregationPipeline(operations), entityClass);
}
protected String replaceWithResourceIfNecessary(String function) {
String func = function;

View File

@@ -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();
}

View File

@@ -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<DBObject> operations = new ArrayList<DBObject>();
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!");

View File

@@ -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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/group/#stage._S_group">
* <code>$group</code>-operation
* </a>
*
* @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<String, DBObject> fields = new HashMap<String, DBObject>();
public GroupOperation(Object id) {
this.id = id;
}
public DBObject getDBObject() {
DBObject projection = new BasicDBObject(ID_KEY, id);
for (Entry<String, DBObject> 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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_addToSet">$addToSet operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$addToSet: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_first">$first operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$first: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_last">$last operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$last: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_max">$max operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$max: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_min">$min operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$min: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_avg">$avg operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$avg: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_push">$push operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$push: "$field"}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum operation</a>
* with a constant value.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: increment}
* }}
* </pre>
* @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
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum operation</a>
* count every item.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: 1}
* }}
* </pre>
* @param name key of the field
* @return
*
*/
public GroupOperation count(String name) {
return count(name, 1);
}
/**
* Adds a field with the
* <a href="http://docs.mongodb.org/manual/reference/aggregation/addToSet/#grp._S_sum">$sum operation</a>.
* <pre>
* {$group: {
* _id: "$id_field",
* name: {$sum: "$field"}
* }}
* </pre>
* @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 <code>$group</code> operation with <code>_id</code> referencing to a field of the document. The
* returned db object equals to <pre>{_id: "$field"}</pre>
* @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 <code>$group</code> 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:
* <pre>
*
* group(idField("path"), idField("pageView", "page.views"), idField("field3"))
*
* </pre>
* which would result in:
* <pre>
*
* {$group: {_id: {path: "$path", pageView: "$page.views", field3: "$field3"}}}
*
* </pre>
* @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 <code>$group</code> operation.
*
* For example:
* <pre>
* {$group: {_id: {key: "$value"}}}
* </pre>
*/
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:
* <pre>
* _id: {field: "$field"}
* </pre>
* @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
* <pre>
* _id: {key: "$field"}
* </pre>
* @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);
}
}
}

View File

@@ -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 <code>$match</code>-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);
}
}

View File

@@ -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<String> reference = new Stack<String>();
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<Object> list = new ArrayList<Object>();
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;

View File

@@ -0,0 +1,45 @@
package org.springframework.data.mongodb.core.aggregation;
import org.springframework.util.Assert;
/**
* Utility class for mongo db reference operator <code>$</code>
*/
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;
}
}