Browse Source

DATAMONGO-1326 - Add support for $lookup to aggregation.

Original Pull Request: #344
CLA: 164120160221125037 (Alessio Fachechi)
pull/347/head
Alessio Fachechi 10 years ago committed by Christoph Strobl
parent
commit
78e99e6df2
  1. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java
  2. 84
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
  3. 38
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
  4. 76
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
  5. 58
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/*
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
/**
* {@link AggregationOperation} that exposes <b>additional</b> {@link ExposedFields} that can be used for later
* aggregation pipeline {@code AggregationOperation}s, e.g. lookup operation produces a field which has to be added to
* the current ones.
*
* @author Alessio Fachechi
*/
public interface AdditionalFieldsExposingAggregationOperation extends FieldsExposingAggregationOperation {
}

84
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java

@ -37,10 +37,11 @@ import com.mongodb.DBObject; @@ -37,10 +37,11 @@ import com.mongodb.DBObject;
/**
* An {@code Aggregation} is a representation of a list of aggregation steps to be performed by the MongoDB Aggregation
* Framework.
*
*
* @author Tobias Trelle
* @author Thomas Darimont
* @author Oliver Gierke
* @author Alessio Fachechi
* @since 1.3
*/
public class Aggregation {
@ -65,7 +66,7 @@ public class Aggregation { @@ -65,7 +66,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
*
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(List<? extends AggregationOperation> operations) {
@ -74,7 +75,7 @@ public class Aggregation { @@ -74,7 +75,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
*
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(AggregationOperation... operations) {
@ -84,7 +85,7 @@ public class Aggregation { @@ -84,7 +85,7 @@ public class Aggregation {
/**
* Returns a copy of this {@link Aggregation} with the given {@link AggregationOptions} set. Note that options are
* supported in MongoDB version 2.6+.
*
*
* @param options must not be {@literal null}.
* @return
* @since 1.6
@ -97,7 +98,7 @@ public class Aggregation { @@ -97,7 +98,7 @@ public class Aggregation {
/**
* Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s.
*
*
* @param type must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
@ -107,7 +108,7 @@ public class Aggregation { @@ -107,7 +108,7 @@ public class Aggregation {
/**
* Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s.
*
*
* @param type must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
@ -117,7 +118,7 @@ public class Aggregation { @@ -117,7 +118,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
*
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(AggregationOperation... aggregationOperations) {
@ -137,7 +138,7 @@ public class Aggregation { @@ -137,7 +138,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
*
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(List<AggregationOperation> aggregationOperations) {
@ -146,7 +147,7 @@ public class Aggregation { @@ -146,7 +147,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
*
* @param aggregationOperations must not be {@literal null} or empty.
* @param options must not be {@literal null} or empty.
*/
@ -162,7 +163,7 @@ public class Aggregation { @@ -162,7 +163,7 @@ public class Aggregation {
/**
* A pointer to the previous {@link AggregationOperation}.
*
*
* @return
*/
public static String previousOperation() {
@ -171,7 +172,7 @@ public class Aggregation { @@ -171,7 +172,7 @@ public class Aggregation {
/**
* Creates a new {@link ProjectionOperation} including the given fields.
*
*
* @param fields must not be {@literal null}.
* @return
*/
@ -181,7 +182,7 @@ public class Aggregation { @@ -181,7 +182,7 @@ public class Aggregation {
/**
* Creates a new {@link ProjectionOperation} includeing the given {@link Fields}.
*
*
* @param fields must not be {@literal null}.
* @return
*/
@ -191,7 +192,7 @@ public class Aggregation { @@ -191,7 +192,7 @@ public class Aggregation {
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
*
*
* @param fieldName must not be {@literal null} or empty.
* @return
*/
@ -201,7 +202,7 @@ public class Aggregation { @@ -201,7 +202,7 @@ public class Aggregation {
/**
* Creates a new {@link GroupOperation} for the given fields.
*
*
* @param fields must not be {@literal null}.
* @return
*/
@ -211,7 +212,7 @@ public class Aggregation { @@ -211,7 +212,7 @@ public class Aggregation {
/**
* Creates a new {@link GroupOperation} for the given {@link Fields}.
*
*
* @param fields must not be {@literal null}.
* @return
*/
@ -221,7 +222,7 @@ public class Aggregation { @@ -221,7 +222,7 @@ public class Aggregation {
/**
* Factory method to create a new {@link SortOperation} for the given {@link Sort}.
*
*
* @param sort must not be {@literal null}.
* @return
*/
@ -231,7 +232,7 @@ public class Aggregation { @@ -231,7 +232,7 @@ public class Aggregation {
/**
* Factory method to create a new {@link SortOperation} for the given sort {@link Direction} and {@code fields}.
*
*
* @param direction must not be {@literal null}.
* @param fields must not be {@literal null}.
* @return
@ -242,7 +243,7 @@ public class Aggregation { @@ -242,7 +243,7 @@ public class Aggregation {
/**
* Creates a new {@link SkipOperation} skipping the given number of elements.
*
*
* @param elementsToSkip must not be less than zero.
* @return
*/
@ -252,7 +253,7 @@ public class Aggregation { @@ -252,7 +253,7 @@ public class Aggregation {
/**
* Creates a new {@link LimitOperation} limiting the result to the given number of elements.
*
*
* @param maxElements must not be less than zero.
* @return
*/
@ -262,7 +263,7 @@ public class Aggregation { @@ -262,7 +263,7 @@ public class Aggregation {
/**
* Creates a new {@link MatchOperation} using the given {@link Criteria}.
*
*
* @param criteria must not be {@literal null}.
* @return
*/
@ -270,12 +271,32 @@ public class Aggregation { @@ -270,12 +271,32 @@ public class Aggregation {
return new MatchOperation(criteria);
}
/**
* Creates a new {@link LookupOperation} for the given fields.
*
* @param fields must not be {@literal null}.
* @return
*/
public static LookupOperation lookup(String from, String localField, String foreignField, String as) {
return lookup(field(from), field(localField), field(foreignField), field(as));
}
/**
* Creates a new {@link LookupOperation} for the given {@link Fields}.
*
* @param fields must not be {@literal null}.
* @return
*/
public static LookupOperation lookup(Field from, Field localField, Field foreignField, Field as) {
return new LookupOperation(from, localField, foreignField, as);
}
/**
* Creates a new {@link Fields} instance for the given field names.
*
* @see Fields#fields(String...)
*
* @param fields must not be {@literal null}.
* @return
* @see Fields#fields(String...)
*/
public static Fields fields(String... fields) {
return Fields.fields(fields);
@ -283,7 +304,7 @@ public class Aggregation { @@ -283,7 +304,7 @@ public class Aggregation {
/**
* Creates a new {@link Fields} instance from the given field name and target reference.
*
*
* @param name must not be {@literal null} or empty.
* @param target must not be {@literal null} or empty.
* @return
@ -295,7 +316,7 @@ public class Aggregation { @@ -295,7 +316,7 @@ public class Aggregation {
/**
* 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}.
* @param distanceField must not be {@literal null} or empty.
* @return
@ -307,7 +328,7 @@ public class Aggregation { @@ -307,7 +328,7 @@ public class Aggregation {
/**
* Returns a new {@link AggregationOptions.Builder}.
*
*
* @return
* @since 1.6
*/
@ -317,7 +338,7 @@ public class Aggregation { @@ -317,7 +338,7 @@ public class Aggregation {
/**
* Converts this {@link Aggregation} specification to a {@link DBObject}.
*
*
* @param inputCollectionName the name of the input collection
* @return the {@code DBObject} representing this aggregation
*/
@ -331,8 +352,11 @@ public class Aggregation { @@ -331,8 +352,11 @@ public class Aggregation {
operationDocuments.add(operation.toDBObject(context));
if (operation instanceof FieldsExposingAggregationOperation) {
boolean additional = operation instanceof AdditionalFieldsExposingAggregationOperation ? true : false;
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext);
context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext,
additional);
}
}
@ -356,7 +380,7 @@ public class Aggregation { @@ -356,7 +380,7 @@ public class Aggregation {
/**
* Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is.
*
*
* @author Oliver Gierke
*/
private static class NoOpAggregationOperationContext implements AggregationOperationContext {
@ -391,7 +415,7 @@ public class Aggregation { @@ -391,7 +415,7 @@ public class Aggregation {
/**
* Describes the system variables available in MongoDB aggregation framework pipeline expressions.
*
*
* @author Thomas Darimont
* @see http://docs.mongodb.org/manual/reference/aggregation-variables
*/
@ -404,7 +428,7 @@ public class Aggregation { @@ -404,7 +428,7 @@ public class Aggregation {
/**
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
* otherwise.
*
*
* @param fieldRef may be {@literal null}.
* @return
*/

38
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java

@ -17,37 +17,57 @@ package org.springframework.data.mongodb.core.aggregation; @@ -17,37 +17,57 @@ package org.springframework.data.mongodb.core.aggregation;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
/**
* {@link AggregationOperationContext} that combines the available field references from a given
* {@code AggregationOperationContext} and an {@link FieldsExposingAggregationOperation}.
*
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Alessio Fachechi
* @since 1.4
*/
class ExposedFieldsAggregationOperationContext implements AggregationOperationContext {
private final ExposedFields exposedFields;
private final AggregationOperationContext rootContext;
private final boolean additional;
/**
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
* {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
*
* @param exposedFields must not be {@literal null}.
* @param rootContext must not be {@literal null}.
*/
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
AggregationOperationContext rootContext) {
this(exposedFields, rootContext, false);
}
/**
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
* {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
*
*
* @param exposedFields must not be {@literal null}.
* @param rootContext must not be {@literal null}.
* @param additional {@literal true} if the context exposes new fields in addition to the previous ones, e.g. in the
* case of a lookup operation, {@literal false} otherwise.
*/
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields, AggregationOperationContext rootContext) {
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields, AggregationOperationContext rootContext,
boolean additional) {
Assert.notNull(exposedFields, "ExposedFields must not be null!");
Assert.notNull(rootContext, "RootContext must not be null!");
this.exposedFields = exposedFields;
this.rootContext = rootContext;
this.additional = additional;
}
/*
@ -79,7 +99,7 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo @@ -79,7 +99,7 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo
/**
* Returns a {@link FieldReference} to the given {@link Field} with the given {@code name}.
*
*
* @param field may be {@literal null}
* @param name must not be {@literal null}
* @return
@ -112,6 +132,16 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo @@ -112,6 +132,16 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo
}
}
if (additional) {
// if no exposed fields found propagate to root context.
if (field != null) {
return rootContext.getReference(field);
} else if (StringUtils.hasText(name)) {
return rootContext.getReference(name);
}
}
throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name));
}
}

76
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.util.Assert;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Encapsulates the aggregation framework {@code $lookup}-operation.
* We recommend to use the static factory method {@link Aggregation#lookup(String, String, String, String)} instead of
* creating instances of this class directly.
*
* @author Alessio Fachechi
* @see http://docs.mongodb.org/manual/reference/aggregation/lookup/#stage._S_lookup
* @since 1.9
*/
public class LookupOperation implements AdditionalFieldsExposingAggregationOperation {
private ExposedField from;
private ExposedField localField;
private ExposedField foreignField;
private ExposedField as;
/**
* Creates a new {@link LookupOperation} for the given {@link Field}s.
*
* @param from must not be {@literal null}.
* @param localField must not be {@literal null}.
* @param foreignField must not be {@literal null}.
* @param as must not be {@literal null}.
*/
public LookupOperation(Field from, Field localField, Field foreignField, Field as) {
Assert.notNull(from, "From must not be null!");
Assert.notNull(localField, "LocalField must not be null!");
Assert.notNull(foreignField, "ForeignField must not be null!");
Assert.notNull(as, "As must not be null!");
this.from = new ExposedField(from, true);
this.localField = new ExposedField(localField, true);
this.foreignField = new ExposedField(foreignField, true);
this.as = new ExposedField(as, true);
}
@Override
public ExposedFields getFields() {
return ExposedFields.from(as);
}
@Override
public DBObject toDBObject(AggregationOperationContext context) {
BasicDBObject lookupObject = new BasicDBObject();
lookupObject.append("from", from.getTarget());
lookupObject.append("localField", localField.getTarget());
lookupObject.append("foreignField", foreignField.getTarget());
lookupObject.append("as", as.getTarget());
return new BasicDBObject("$lookup", lookupObject);
}
}

58
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import com.mongodb.DBObject;
import org.junit.Test;
import org.springframework.data.mongodb.core.DBObjectTestUtils;
/**
* Unit tests for {@link LookupOperation}.
*
* @author Alessio Fachechi
*/
public class LookupOperationUnitTests {
@Test(expected = IllegalArgumentException.class)
public void rejectsNullFields() {
new LookupOperation((Field) null, (Field) null, (Field) null, (Field) null);
}
@Test
public void lookupOperationWithValues() {
LookupOperation lookupOperation = Aggregation.lookup("a", "b", "c", "d");
DBObject lookupClause = extractDbObjectFromLookupOperation(lookupOperation);
assertThat((String) lookupClause.get("from"), is(new String("a")));
assertThat((String) lookupClause.get("localField"), is(new String("b")));
assertThat((String) lookupClause.get("foreignField"), is(new String("c")));
assertThat((String) lookupClause.get("as"), is(new String("d")));
assertThat(lookupOperation.getFields().exposesNoFields(), is(false));
assertThat(lookupOperation.getFields().exposesSingleFieldOnly(), is(true));
}
private DBObject extractDbObjectFromLookupOperation(LookupOperation lookupOperation) {
DBObject dbObject = lookupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
DBObject lookupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$lookup");
return lookupClause;
}
}
Loading…
Cancel
Save