DATAMONGO-861 - Add support for $cond and $ifNull operators in aggregation operations.

We now support $cond and $ifNull operators for projection and grouping operations. ConditionalOperator and IfNullOperators are AggregationExpressions that can be applied to transform or generate values during aggregation.

TypedAggregation<InventoryItem> agg = newAggregation(InventoryItem.class,
  project().and("discount")
    .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250))
      .then(30)
      .otherwise(20))
    .and(ifNull("description", "Unspecified")).as("description")
);

corresponds to

{ "$project": { "discount": { "$cond": { "if": { "$gte": [ "$qty", 250 ] },
        "then": 30, "else": 20 } },
    "description": { "$ifNull": [ "$description", "Unspecified"] }
  }
}

Original Pull Request: #385
This commit is contained in:
Mark Paluch
2016-07-08 16:44:13 +02:00
committed by Christoph Strobl
parent 116dda63c2
commit ace01e4e6d
11 changed files with 1527 additions and 7 deletions

View File

@@ -372,6 +372,68 @@ public class Aggregation {
return new LookupOperation(from, localField, foreignField, as); return new LookupOperation(from, localField, foreignField, as);
} }
/**
* Creates a new {@link IfNullOperator} for the given {@code field} and {@code replacement} value.
*
* @param field must not be {@literal null}.
* @param replacement must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public static IfNullOperator ifNull(String field, Object replacement) {
return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement);
}
/**
* Creates a new {@link IfNullOperator} for the given {@link Field} and {@link Field} to obtain a value from.
*
* @param field must not be {@literal null}.
* @param replacement must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public static IfNullOperator ifNull(Field field, Field replacement) {
return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement);
}
/**
* Creates a new {@link IfNullOperator} for the given {@link Field} and {@code replacement} value.
*
* @param field must not be {@literal null}.
* @param replacement must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public static IfNullOperator ifNull(Field field, Object replacement) {
return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement);
}
/**
* Creates a new {@link ConditionalOperator} for the given {@link Field} that holds a {@literal boolean} value.
*
* @param booleanField must not be {@literal null}.
* @param then must not be {@literal null}.
* @param otherwise must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public static ConditionalOperator conditional(Field booleanField, Object then, Object otherwise) {
return ConditionalOperator.newBuilder().when(booleanField).then(then).otherwise(otherwise);
}
/**
* Creates a new {@link ConditionalOperator} for the given {@link Criteria}.
*
* @param criteria must not be {@literal null}.
* @param then must not be {@literal null}.
* @param otherwise must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public static ConditionalOperator conditional(Criteria criteria, Object then, Object otherwise) {
return ConditionalOperator.newBuilder().when(criteria).then(then).otherwise(otherwise);
}
/** /**
* Creates a new {@link Fields} instance for the given field names. * Creates a new {@link Fields} instance for the given field names.
* *

View File

@@ -0,0 +1,381 @@
/*
* 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.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Encapsulates the aggregation framework {@code $cond} operator. A {@link ConditionalOperator} allows nested conditions
* {@code if-then[if-then-else]-else} using {@link Field}, {@link CriteriaDefinition} or a {@link DBObject custom}
* condition. Replacement values can be either {@link Field field references}, values of simple MongoDB types or values
* that can be converted to a simple MongoDB type.
*
* @see http://docs.mongodb.com/manual/reference/operator/aggregation/cond/
* @author Mark Paluch
* @since 1.10
*/
public class ConditionalOperator implements AggregationExpression {
private final Object condition;
private final Object thenValue;
private final Object otherwiseValue;
/**
* Creates a new {@link ConditionalOperator} for a given {@link Field} and {@code then}/{@code otherwise} values.
*
* @param condition must not be {@literal null}.
* @param thenValue must not be {@literal null}.
* @param otherwiseValue must not be {@literal null}.
*/
public ConditionalOperator(Field condition, Object thenValue, Object otherwiseValue) {
this((Object) condition, thenValue, otherwiseValue);
}
/**
* Creates a new {@link ConditionalOperator} for a given {@link CriteriaDefinition} and {@code then}/{@code otherwise}
* values.
*
* @param condition must not be {@literal null}.
* @param thenValue must not be {@literal null}.
* @param otherwiseValue must not be {@literal null}.
*/
public ConditionalOperator(CriteriaDefinition condition, Object thenValue, Object otherwiseValue) {
this((Object) condition, thenValue, otherwiseValue);
}
/**
* Creates a new {@link ConditionalOperator} for a given {@link DBObject criteria} and {@code then}/{@code otherwise}
* values.
*
* @param condition must not be {@literal null}.
* @param thenValue must not be {@literal null}.
* @param otherwiseValue must not be {@literal null}.
*/
public ConditionalOperator(DBObject condition, Object thenValue, Object otherwiseValue) {
this((Object) condition, thenValue, otherwiseValue);
}
private ConditionalOperator(Object condition, Object thenValue, Object otherwiseValue) {
Assert.notNull(condition, "Condition must not be null!");
Assert.notNull(thenValue, "'Then value' must not be null!");
Assert.notNull(otherwiseValue, "'Otherwise value' must not be null!");
assertNotBuilder(condition, "Condition");
assertNotBuilder(thenValue, "'Then value'");
assertNotBuilder(otherwiseValue, "'Otherwise value'");
this.condition = condition;
this.thenValue = thenValue;
this.otherwiseValue = otherwiseValue;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public DBObject toDbObject(AggregationOperationContext context) {
BasicDBObject condObject = new BasicDBObject();
condObject.append("if", resolveCriteria(context, condition));
condObject.append("then", resolveValue(context, thenValue));
condObject.append("else", resolveValue(context, otherwiseValue));
return new BasicDBObject("$cond", condObject);
}
private Object resolveValue(AggregationOperationContext context, Object value) {
if (value instanceof DBObject || value instanceof Field) {
return resolve(context, value);
}
if (value instanceof ConditionalOperator) {
return ((ConditionalOperator) value).toDbObject(context);
}
DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value));
return toMap.get("$set");
}
private Object resolveCriteria(AggregationOperationContext context, Object value) {
if (value instanceof DBObject || value instanceof Field) {
return resolve(context, value);
}
if (value instanceof CriteriaDefinition) {
DBObject mappedObject = context.getMappedObject(((CriteriaDefinition) value).getCriteriaObject());
BasicDBList clauses = new BasicDBList();
clauses.addAll(getClauses(context, mappedObject));
if (clauses.size() == 1) {
return clauses.get(0);
}
return clauses;
}
throw new InvalidDataAccessApiUsageException(
String.format("Invalid value in condition. Supported: DBObject, Field references, Criteria, got: %s", value));
}
private BasicDBList getClauses(AggregationOperationContext context, DBObject mappedObject) {
BasicDBList clauses = new BasicDBList();
for (String key : mappedObject.keySet()) {
Object predicate = mappedObject.get(key);
clauses.addAll(getClauses(context, key, predicate));
}
return clauses;
}
private BasicDBList getClauses(AggregationOperationContext context, String key, Object predicate) {
BasicDBList clauses = new BasicDBList();
if (predicate instanceof BasicDBList) {
BasicDBList args = new BasicDBList();
for (Object clause : (BasicDBList) predicate) {
args.addAll(getClauses(context, (BasicDBObject) clause));
}
clauses.add(new BasicDBObject(key, args));
} else if (predicate instanceof DBObject) {
DBObject nested = (DBObject) predicate;
for (String s : nested.keySet()) {
if (!isKeyword(s)) {
continue;
}
BasicDBList args = new BasicDBList();
args.add("$" + key);
args.add(nested.get(s));
clauses.add(new BasicDBObject(s, args));
}
} else if (!isKeyword(key)) {
BasicDBList args = new BasicDBList();
args.add("$" + key);
args.add(predicate);
clauses.add(new BasicDBObject("$eq", args));
}
return clauses;
}
/**
* Returns whether the given {@link String} is a MongoDB keyword.
*
* @param candidate
* @return
*/
private boolean isKeyword(String candidate) {
return candidate.startsWith("$");
}
private Object resolve(AggregationOperationContext context, Object value) {
if (value instanceof DBObject) {
return context.getMappedObject((DBObject) value);
}
return context.getReference((Field) value).toString();
}
private void assertNotBuilder(Object toCheck, String name) {
Assert.isTrue(!ClassUtils.isAssignableValue(ConditionalExpressionBuilder.class, toCheck),
String.format("%s must not be of type %s", name, ConditionalExpressionBuilder.class.getSimpleName()));
}
/**
* Get a builder that allows fluent creation of {@link ConditionalOperator}.
*
* @return a new {@link ConditionalExpressionBuilder}.
*/
public static ConditionalExpressionBuilder newBuilder() {
return ConditionalExpressionBuilder.newBuilder();
}
public static interface WhenBuilder {
/**
* @param booleanExpression expression that yields in a boolean result, must not be {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder when(DBObject booleanExpression);
/**
* @param booleanField reference to a field holding a boolean value, must not be {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder when(Field booleanField);
/**
* @param booleanField name of a field holding a boolean value, must not be {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder when(String booleanField);
/**
* @param criteria criteria to evaluate, must not be {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder when(CriteriaDefinition criteria);
}
public static interface ThenBuilder {
/**
* @param value the value to be used if the condition evaluates {@literal true}. Can be a {@link DBObject}, a value
* that is supported by MongoDB or a value that can be converted to a MongoDB representation but must not
* be {@literal null}.
* @return the {@link OtherwiseBuilder}
*/
OtherwiseBuilder then(Object value);
}
public static interface OtherwiseBuilder {
/**
* @param value the value to be used if the condition evaluates {@literal false}. Can be a {@link DBObject}, a value
* that is supported by MongoDB or a value that can be converted to a MongoDB representation but must not
* be {@literal null}.
* @return the {@link ConditionalOperator}
*/
ConditionalOperator otherwise(Object value);
}
/**
* Builder for fluent {@link ConditionalOperator} creation.
*
* @author Mark Paluch
* @since 1.10
*/
public static final class ConditionalExpressionBuilder implements WhenBuilder, ThenBuilder, OtherwiseBuilder {
private Object condition;
private Object thenValue;
private ConditionalExpressionBuilder() {}
/**
* Creates a new builder for {@link ConditionalOperator}.
*
* @return never {@literal null}.
*/
public static ConditionalExpressionBuilder newBuilder() {
return new ConditionalExpressionBuilder();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(com.mongodb.DBObject)
*/
@Override
public ConditionalExpressionBuilder when(DBObject booleanExpression) {
Assert.notNull(booleanExpression, "'Boolean expression' must not be null!");
this.condition = booleanExpression;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.query.CriteriaDefinition)
*/
@Override
public ThenBuilder when(CriteriaDefinition criteria) {
Assert.notNull(criteria, "Criteria must not be null!");
this.condition = criteria;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.aggregation.Field)
*/
@Override
public ThenBuilder when(Field booleanField) {
Assert.notNull(booleanField, "Boolean field must not be null!");
this.condition = booleanField;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(java.lang.String)
*/
@Override
public ThenBuilder when(String booleanField) {
Assert.hasText(booleanField, "Boolean field name must not be null or empty!");
this.condition = Fields.field(booleanField);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.ThenBuilder#then(java.lang.Object)
*/
@Override
public OtherwiseBuilder then(Object thenValue) {
Assert.notNull(thenValue, "'Then-value' must not be null!");
this.thenValue = thenValue;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.OtherwiseBuilder#otherwise(java.lang.Object)
*/
@Override
public ConditionalOperator otherwise(Object otherwiseValue) {
Assert.notNull(otherwiseValue, "'Otherwise-value' must not be null!");
return new ConditionalOperator(condition, thenValue, otherwiseValue);
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.util.Assert;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Encapsulates the aggregation framework {@code $ifNull} operator. Replacement values can be either {@link Field field
* references}, values of simple MongoDB types or values that can be converted to a simple MongoDB type.
*
* @see http://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/
* @author Mark Paluch
* @since 1.10
*/
public class IfNullOperator implements AggregationExpression {
private final Field field;
private final Object value;
/**
* Creates a new {@link IfNullOperator} for the given {@link Field} and replacement {@code value}.
*
* @param field must not be {@literal null}.
* @param value must not be {@literal null}.
*/
public IfNullOperator(Field field, Object value) {
Assert.notNull(field, "Field must not be null!");
Assert.notNull(value, "'Replacement-value' must not be null!");
this.field = field;
this.value = value;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public DBObject toDbObject(AggregationOperationContext context) {
BasicDBList list = new BasicDBList();
list.add(context.getReference(field).toString());
if (value instanceof Field) {
list.add(context.getReference((Field) value).toString());
} else {
DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value));
list.add(toMap.get("$set"));
}
return new BasicDBObject("$ifNull", list);
}
/**
* Get a builder that allows fluent creation of {@link IfNullOperator}.
*
* @return a new {@link IfNullBuilder}.
*/
public static IfNullBuilder newBuilder() {
return IfNullOperatorBuilder.newBuilder();
}
public static interface IfNullBuilder {
/**
* @param field the field to check for a {@literal null} value, field reference must not be {@literal null}.
* @return the {@link ThenBuilder}
*/
ThenBuilder ifNull(Field field);
/**
* @param field the field to check for a {@literal null} value, field name must not be {@literal null} or empty.
* @return the {@link ThenBuilder}
*/
ThenBuilder ifNull(String field);
}
public static interface ThenBuilder {
/**
* @param field the field holding the replacement value, must not be {@literal null}.
* @return the {@link IfNullOperator}
*/
IfNullOperator thenReplaceWith(Field field);
/**
* @param value the value to be used if the {@code $ifNull }condition evaluates {@literal true}. Can be a
* {@link DBObject}, a value that is supported by MongoDB or a value that can be converted to a MongoDB
* representation but must not be {@literal null}.
* @return the {@link IfNullOperator}
*/
IfNullOperator thenReplaceWith(Object value);
}
/**
* Builder for fluent {@link IfNullOperator} creation.
*
* @author Mark Paluch
* @since 1.10
*/
public static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder {
private Field field;
private IfNullOperatorBuilder() {}
/**
* Creates a new builder for {@link IfNullOperator}.
*
* @return never {@literal null}.
*/
public static IfNullOperatorBuilder newBuilder() {
return new IfNullOperatorBuilder();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(org.springframework.data.mongodb.core.aggregation.Field)
*/
public ThenBuilder ifNull(Field field) {
Assert.notNull(field, "Field must not be null!");
this.field = field;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(java.lang.String)
*/
public ThenBuilder ifNull(String name) {
Assert.hasText(name, "Field name must not be null or empty!");
this.field = Fields.field(name);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(org.springframework.data.mongodb.core.aggregation.Field)
*/
@Override
public IfNullOperator thenReplaceWith(Field replacementField) {
Assert.notNull(replacementField, "Replacement field must not be null!");
return new IfNullOperator(this.field, replacementField);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(java.lang.Object)
*/
@Override
public IfNullOperator thenReplaceWith(Object value) {
Assert.notNull(value, "'Replacement-value' must not be null!");
return new IfNullOperator(this.field, value);
}
}
}

View File

@@ -104,8 +104,8 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
*/ */
private ProjectionOperation andReplaceLastOneWith(Projection projection) { private ProjectionOperation andReplaceLastOneWith(Projection projection) {
List<Projection> projections = this.projections.isEmpty() ? Collections.<Projection> emptyList() : this.projections List<Projection> projections = this.projections.isEmpty() ? Collections.<Projection> emptyList()
.subList(0, this.projections.size() - 1); : this.projections.subList(0, this.projections.size() - 1);
return new ProjectionOperation(projections, Arrays.asList(projection)); return new ProjectionOperation(projections, Arrays.asList(projection));
} }
@@ -240,6 +240,24 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return * @return
*/ */
public abstract ProjectionOperation as(String alias); public abstract ProjectionOperation as(String alias);
/**
* Apply a conditional projection using {@link ConditionalOperator}.
*
* @param conditional must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public abstract ProjectionOperation transform(ConditionalOperator conditional);
/**
* Apply a conditional value replacement for {@literal null} values using {@link IfNullOperator}.
*
* @param ifNull must not be {@literal null}.
* @return never {@literal null}.
* @since 1.10
*/
public abstract ProjectionOperation transform(IfNullOperator ifNull);
} }
/** /**
@@ -370,7 +388,8 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @param operation must not be {@literal null}. * @param operation must not be {@literal null}.
* @param previousProjection the previous operation projection, may be {@literal null}. * @param previousProjection the previous operation projection, may be {@literal null}.
*/ */
public ProjectionOperationBuilder(String name, ProjectionOperation operation, OperationProjection previousProjection) { public ProjectionOperationBuilder(String name, ProjectionOperation operation,
OperationProjection previousProjection) {
super(name, operation); super(name, operation);
this.name = name; this.name = name;
@@ -419,7 +438,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
/** /**
* Allows to specify an alias for the previous projection operation. * Allows to specify an alias for the previous projection operation.
* *
* @param string * @param alias
* @return * @return
*/ */
@Override @Override
@@ -436,6 +455,24 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return this.operation.and(new FieldProjection(Fields.field(alias, name), null)); return this.operation.and(new FieldProjection(Fields.field(alias, name), null));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.ConditionalOperator)
*/
@Override
public ProjectionOperation transform(ConditionalOperator conditional) {
return this.operation.and(new ExpressionProjection(Fields.field(name), conditional));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.IfNullOperator)
*/
@Override
public ProjectionOperation transform(IfNullOperator ifNull) {
return this.operation.and(new ExpressionProjection(Fields.field(name), ifNull));
}
/** /**
* Generates an {@code $add} expression that adds the given number to the previously mentioned field. * Generates an {@code $add} expression that adds the given number to the previously mentioned field.
* *
@@ -764,7 +801,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
this.values = Arrays.asList(values); this.values = Arrays.asList(values);
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/ */

View File

@@ -20,6 +20,7 @@ import static org.junit.Assert.*;
import static org.junit.Assume.*; import static org.junit.Assume.*;
import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.domain.Sort.Direction.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.aggregation.Fields.*;
import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
@@ -55,6 +56,7 @@ import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.Venue;
import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry;
import org.springframework.data.mongodb.core.index.GeospatialIndex; import org.springframework.data.mongodb.core.index.GeospatialIndex;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
@@ -131,6 +133,8 @@ public class AggregationTests {
mongoTemplate.dropCollection(Reservation.class); mongoTemplate.dropCollection(Reservation.class);
mongoTemplate.dropCollection(Venue.class); mongoTemplate.dropCollection(Venue.class);
mongoTemplate.dropCollection(MeterData.class); mongoTemplate.dropCollection(MeterData.class);
mongoTemplate.dropCollection(LineItem.class);
mongoTemplate.dropCollection(InventoryItem.class);
} }
/** /**
@@ -484,6 +488,250 @@ public class AggregationTests {
assertThat(stateStats.totalPopulation, is(29760021)); assertThat(stateStats.totalPopulation, is(29760021));
} }
/**
* @see DATAMONGO-861
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/cond/#example
*/
@Test
public void aggregationUsingConditionalProjectionToCalculateDiscount() {
/*
db.inventory.aggregate(
[
{
$project:
{
item: 1,
discount:
{
$cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
}
}
}
]
)
*/
mongoTemplate.insert(new InventoryItem(1, "abc1", 300));
mongoTemplate.insert(new InventoryItem(2, "abc2", 200));
mongoTemplate.insert(new InventoryItem(3, "xyz1", 250));
TypedAggregation<InventoryItem> aggregation = newAggregation(InventoryItem.class, //
project("item") //
.and("discount")//
.transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) //
.then(30) //
.otherwise(20)));
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(3));
DBObject first = result.getMappedResults().get(0);
assertThat(first.get("_id"), is((Object) 1));
assertThat(first.get("discount"), is((Object) 30));
DBObject second = result.getMappedResults().get(1);
assertThat(second.get("_id"), is((Object) 2));
assertThat(second.get("discount"), is((Object) 20));
DBObject third = result.getMappedResults().get(2);
assertThat(third.get("_id"), is((Object) 3));
assertThat(third.get("discount"), is((Object) 30));
}
/**
* @see DATAMONGO-861
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/#example
*/
@Test
public void aggregationUsingIfNullToProjectSaneDefaults() {
/*
db.inventory.aggregate(
[
{
$project: {
item: 1,
description: { $ifNull: [ "$description", "Unspecified" ] }
}
}
]
)
*/
mongoTemplate.insert(new InventoryItem(1, "abc1", "product 1", 300));
mongoTemplate.insert(new InventoryItem(2, "abc2", 200));
mongoTemplate.insert(new InventoryItem(3, "xyz1", 250));
TypedAggregation<InventoryItem> aggregation = newAggregation(InventoryItem.class, //
project("item") //
.and(ifNull("description", "Unspecified")) //
.as("description")//
);
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(3));
DBObject first = result.getMappedResults().get(0);
assertThat(first.get("_id"), is((Object) 1));
assertThat(first.get("description"), is((Object) "product 1"));
DBObject second = result.getMappedResults().get(1);
assertThat(second.get("_id"), is((Object) 2));
assertThat(second.get("description"), is((Object) "Unspecified"));
}
/**
* @see DATAMONGO-861
*/
@Test
public void aggregationUsingConditionalProjection() {
TypedAggregation<ZipInfo> aggregation = newAggregation(ZipInfo.class, //
project() //
.and("largePopulation")//
.transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) //
.then(true) //
.otherwise(false)) //
.and("population").as("population"));
assertThat(aggregation, is(notNullValue()));
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(29467));
DBObject firstZipInfoStats = result.getMappedResults().get(0);
assertThat(firstZipInfoStats.get("largePopulation"), is((Object) false));
assertThat(firstZipInfoStats.get("population"), is((Object) 6055));
}
/**
* @see DATAMONGO-861
*/
@Test
public void aggregationUsingNestedConditionalProjection() {
TypedAggregation<ZipInfo> aggregation = newAggregation(ZipInfo.class, //
project() //
.and("size")//
.transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) //
.then(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(200000)).then("huge")
.otherwise("small")) //
.otherwise("small")) //
.and("population").as("population"));
assertThat(aggregation, is(notNullValue()));
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(29467));
DBObject firstZipInfoStats = result.getMappedResults().get(0);
assertThat(firstZipInfoStats.get("size"), is((Object) "small"));
assertThat(firstZipInfoStats.get("population"), is((Object) 6055));
}
/**
* @see DATAMONGO-861
*/
@Test
public void aggregationUsingIfNullProjection() {
mongoTemplate.insert(new LineItem("id", "caption", 0));
mongoTemplate.insert(new LineItem("idonly", null, 0));
TypedAggregation<LineItem> aggregation = newAggregation(LineItem.class, //
project("id") //
.and("caption")//
.transform(ifNull(field("caption"), "unknown")),
sort(ASC, "id"));
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(2));
DBObject id = result.getMappedResults().get(0);
assertThat((String) id.get("caption"), is(equalTo("caption")));
DBObject idonly = result.getMappedResults().get(1);
assertThat((String) idonly.get("caption"), is(equalTo("unknown")));
}
/**
* @see DATAMONGO-861
*/
@Test
public void aggregationUsingIfNullReplaceWithFieldReferenceProjection() {
mongoTemplate.insert(new LineItem("id", "caption", 0));
mongoTemplate.insert(new LineItem("idonly", null, 0));
TypedAggregation<LineItem> aggregation = newAggregation(LineItem.class, //
project("id") //
.and("caption")//
.transform(ifNull(field("caption"), field("id"))),
sort(ASC, "id"));
assertThat(aggregation.toString(), is(notNullValue()));
AggregationResults<DBObject> result = mongoTemplate.aggregate(aggregation, DBObject.class);
assertThat(result.getMappedResults().size(), is(2));
DBObject id = result.getMappedResults().get(0);
assertThat((String) id.get("caption"), is(equalTo("caption")));
DBObject idonly = result.getMappedResults().get(1);
assertThat((String) idonly.get("caption"), is(equalTo("idonly")));
}
/**
* @see DATAMONGO-861
*/
@Test
public void shouldAllowGroupingUsingConditionalExpressions() {
mongoTemplate.dropCollection(CarPerson.class);
CarPerson person1 = new CarPerson("first1", "last1", new CarDescriptor.Entry("MAKE1", "MODEL1", 2000),
new CarDescriptor.Entry("MAKE1", "MODEL2", 2001));
CarPerson person2 = new CarPerson("first2", "last2", new CarDescriptor.Entry("MAKE3", "MODEL4", 2014));
CarPerson person3 = new CarPerson("first3", "last3", new CarDescriptor.Entry("MAKE2", "MODEL5", 2015));
mongoTemplate.save(person1);
mongoTemplate.save(person2);
mongoTemplate.save(person3);
TypedAggregation<CarPerson> agg = Aggregation.newAggregation(CarPerson.class,
unwind("descriptors.carDescriptor.entries"), //
project() //
.and(new ConditionalOperator(Criteria.where("descriptors.carDescriptor.entries.make").is("MAKE1"), "good",
"meh"))
.as("make") //
.and("descriptors.carDescriptor.entries.model").as("model") //
.and("descriptors.carDescriptor.entries.year").as("year"), //
group("make").avg(new ConditionalOperator(Criteria.where("year").gte(2012), 1, 9000)).as("score"),
sort(ASC, "make"));
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, DBObject.class);
assertThat(result.getMappedResults(), hasSize(2));
DBObject meh = result.getMappedResults().get(0);
assertThat((String) meh.get("_id"), is(equalTo("meh")));
assertThat(((Number) meh.get("score")).longValue(), is(equalTo(1L)));
DBObject good = result.getMappedResults().get(1);
assertThat((String) good.get("_id"), is(equalTo("good")));
assertThat(((Number) good.get("score")).longValue(), is(equalTo(9000L)));
}
/** /**
* @see http://docs.mongodb.org/manual/tutorial/aggregation-examples/#return-the-five-most-common-likes * @see http://docs.mongodb.org/manual/tutorial/aggregation-examples/#return-the-five-most-common-likes
*/ */
@@ -1460,4 +1708,31 @@ public class AggregationTests {
this.dateValue = dateValue; this.dateValue = dateValue;
} }
} }
/**
* @see DATAMONGO-861
*/
@Document(collection = "inventory")
static class InventoryItem {
int id;
String item;
String description;
int qty;
public InventoryItem() {}
public InventoryItem(int id, String item, int qty) {
this.id = id;
this.item = item;
this.qty = qty;
}
public InventoryItem(int id, String item, String description, int qty) {
this.id = id;
this.item = item;
this.description = description;
this.qty = qty;
}
}
} }

View File

@@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.aggregation.Fields.*;
import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
@@ -29,7 +30,10 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder; import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject; import com.mongodb.DBObject;
@@ -243,6 +247,25 @@ public class AggregationUnitTests {
assertThat(fields.get("foosum"), is((Object) new BasicDBObject("$sum", "$foo"))); assertThat(fields.get("foosum"), is((Object) new BasicDBObject("$sum", "$foo")));
} }
/**
* @see DATAMONGO-861
*/
@Test
public void conditionExpressionBasedFieldsShouldBeReferencableInFollowingOperations() {
DBObject agg = newAggregation( //
project("a"), //
group("a").first(conditional(Criteria.where("a").gte(42), "answer", "no-answer")).as("foosum") //
).toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
DBObject secondProjection = ((List<DBObject>) agg.get("pipeline")).get(1);
DBObject fields = getAsDBObject(secondProjection, "$group");
assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first"));
assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "answer"));
assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer"));
}
/** /**
* @see DATAMONGO-908 * @see DATAMONGO-908
*/ */
@@ -379,6 +402,160 @@ public class AggregationUnitTests {
assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get())); assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get()));
} }
/**
* @see DATAMONGO-861
*/
@Test
public void shouldRenderProjectionConditionalExpressionCorrectly() {
DBObject agg = Aggregation.newAggregation(//
project().and(ConditionalOperator.newBuilder() //
.when("isYellow") //
.then("bright") //
.otherwise("dark")).as("color"))
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 0, "$project");
DBObject expectedCondition = new BasicDBObject() //
.append("if", "$isYellow") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void shouldRenderProjectionConditionalCorrectly() {
DBObject agg = Aggregation.newAggregation(//
project().and("color")
.transform(ConditionalOperator.newBuilder() //
.when("isYellow") //
.then("bright") //
.otherwise("dark")))
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 0, "$project");
DBObject expectedCondition = new BasicDBObject() //
.append("if", "$isYellow") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void shouldRenderProjectionConditionalWithCriteriaCorrectly() {
DBObject agg = Aggregation
.newAggregation(project()//
.and("color")//
.transform(conditional(Criteria.where("key").gt(5), "bright", "dark"))) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 0, "$project");
DBObject expectedCondition = new BasicDBObject() //
.append("if", new BasicDBObject("$gt", new BasicDbListBuilder().add("$key").add(5).get())) //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void referencingProjectionAliasesShouldRenderProjectionConditionalWithFieldReferenceCorrectly() {
DBObject agg = Aggregation
.newAggregation(//
project().and("color").as("chroma"),
project().and("luminosity") //
.transform(conditional(field("chroma"), "bright", "dark"))) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 1, "$project");
DBObject expectedCondition = new BasicDBObject() //
.append("if", "$chroma") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void referencingProjectionAliasesShouldRenderProjectionConditionalWithCriteriaReferenceCorrectly() {
DBObject agg = Aggregation
.newAggregation(//
project().and("color").as("chroma"),
project().and("luminosity") //
.transform(conditional(Criteria.where("chroma").is(100), "bright", "dark"))) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 1, "$project");
DBObject expectedCondition = new BasicDBObject() //
.append("if", new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(100).get())) //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void shouldRenderProjectionIfNullWithFieldReferenceCorrectly() {
DBObject agg = Aggregation
.newAggregation(//
project().and("color"), //
project().and("luminosity") //
.transform(ifNull(field("chroma"), "unknown"))) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 1, "$project");
BasicDBList expectedCondition = new BasicDbListBuilder() //
.add("$chroma") //
.add("unknown")//
.get();
assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void shouldRenderProjectionIfNullWithFallbackFieldReferenceCorrectly() {
DBObject agg = Aggregation
.newAggregation(//
project("fallback").and("color").as("chroma"),
project().and("luminosity") //
.transform(ifNull(field("chroma"), field("fallback")))) //
.toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
DBObject project = extractPipelineElement(agg, 1, "$project");
BasicDBList expectedCondition = new BasicDbListBuilder() //
.add("$chroma") //
.add("$fallback")//
.get();
assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition));
}
private DBObject extractPipelineElement(DBObject agg, int index, String operation) { private DBObject extractPipelineElement(DBObject agg, int index, String operation) {
List<DBObject> pipeline = (List<DBObject>) agg.get("pipeline"); List<DBObject> pipeline = (List<DBObject>) agg.get("pipeline");

View File

@@ -0,0 +1,210 @@
/*
* 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.junit.Assert.*;
import static org.springframework.data.mongodb.core.aggregation.ConditionalOperator.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import org.junit.Test;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Unit tests for {@link ConditionalOperator}.
*
* @author Mark Paluch
*/
public class ConditionalOperatorUnitTests {
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void shouldRejectNullCondition() {
new ConditionalOperator((Field) null, "", "");
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void shouldRejectThenValue() {
new ConditionalOperator(Fields.field("field"), null, "");
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void shouldRejectOtherwiseValue() {
new ConditionalOperator(Fields.field("field"), "", null);
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void builderRejectsEmptyFieldName() {
newBuilder().when("");
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void builderRejectsNullFieldName() {
newBuilder().when((DBObject) null);
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void builderRejectsNullCriteriaName() {
newBuilder().when((Criteria) null);
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void builderRejectsBuilderAsThenValue() {
newBuilder().when("isYellow").then(newBuilder().when("field").then("then-value")).otherwise("otherwise");
}
/**
* @see DATAMONGO-861
*/
@Test
public void simpleBuilderShouldRenderCorrectly() {
ConditionalOperator operator = newBuilder().when("isYellow").then("bright").otherwise("dark");
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
DBObject expectedCondition = new BasicDBObject() //
.append("if", "$isYellow") //
.append("then", "bright") //
.append("else", "dark");
assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void simpleCriteriaShouldRenderCorrectly() {
ConditionalOperator operator = newBuilder().when(Criteria.where("luminosity").gte(100)).then("bright")
.otherwise("dark");
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
DBObject expectedCondition = new BasicDBObject() //
.append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get())) //
.append("then", "bright") //
.append("else", "dark");
assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void andCriteriaShouldRenderCorrectly() {
ConditionalOperator operator = newBuilder() //
.when(Criteria.where("luminosity").gte(100) //
.andOperator(Criteria.where("hue").is(50), //
Criteria.where("saturation").lt(11)))
.then("bright").otherwise("dark");
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
BasicDBObject luminosity = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get());
BasicDBObject hue = new BasicDBObject("$eq", new BasicDbListBuilder().add("$hue").add(50).get());
BasicDBObject saturation = new BasicDBObject("$lt", new BasicDbListBuilder().add("$saturation").add(11).get());
DBObject expectedCondition = new BasicDBObject() //
.append("if",
new BasicDbListBuilder().add(luminosity)
.add(new BasicDBObject("$and", new BasicDbListBuilder().add(hue).add(saturation).get())).get()) //
.append("then", "bright") //
.append("else", "dark");
assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void twoArgsCriteriaShouldRenderCorrectly() {
Criteria criteria = Criteria.where("luminosity").gte(100) //
.and("saturation").and("chroma").is(200);
ConditionalOperator operator = newBuilder().when(criteria).then("bright").otherwise("dark");
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
BasicDBObject gte = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get());
BasicDBObject is = new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(200).get());
DBObject expectedCondition = new BasicDBObject() //
.append("if", new BasicDbListBuilder().add(gte).add(is).get()) //
.append("then", "bright") //
.append("else", "dark");
assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition));
}
/**
* @see DATAMONGO-861
*/
@Test
public void nestedCriteriaShouldRenderCorrectly() {
ConditionalOperator operator = newBuilder() //
.when(Criteria.where("luminosity").gte(100)) //
.then(newBuilder() //
.when(Criteria.where("luminosity").gte(200)) //
.then("verybright") //
.otherwise("not-so-bright")) //
.otherwise(newBuilder() //
.when(Criteria.where("luminosity").lt(50)) //
.then("very-dark") //
.otherwise("not-so-dark"));
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
DBObject trueCondition = new BasicDBObject() //
.append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(200).get())) //
.append("then", "verybright") //
.append("else", "not-so-bright");
DBObject falseCondition = new BasicDBObject() //
.append("if", new BasicDBObject("$lt", new BasicDbListBuilder().add("$luminosity").add(50).get())) //
.append("then", "very-dark") //
.append("else", "not-so-dark");
assertThat(dbObject, isBsonObject().containing("$cond.then.$cond", trueCondition));
assertThat(dbObject, isBsonObject().containing("$cond.else.$cond", falseCondition));
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.junit.Assert.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import org.junit.Test;
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
import com.mongodb.BasicDBList;
import com.mongodb.DBObject;
/**
* Unit tests for {@link IfNullOperator}.
*
* @author Mark Paluch
*/
public class IfNullOperatorUnitTests {
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void shouldRejectNullCondition() {
new IfNullOperator(null, "");
}
/**
* @see DATAMONGO-861
*/
@Test(expected = IllegalArgumentException.class)
public void shouldRejectThenValue() {
new IfNullOperator(Fields.field("aa"), null);
}
/**
* @see DATAMONGO-861
*/
@Test
public void simpleIfNullShouldRenderCorrectly() {
IfNullOperator operator = IfNullOperator.newBuilder() //
.ifNull("optional") //
.thenReplaceWith("a more sophisticated value");
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
BasicDBList expected = new BasicDbListBuilder() //
.add("$optional") //
.add("a more sophisticated value")//
.get();
assertThat(dbObject, isBsonObject().containing("$ifNull", expected));
}
/**
* @see DATAMONGO-861
*/
@Test
public void fieldReplacementIfNullShouldRenderCorrectly() {
IfNullOperator operator = IfNullOperator.newBuilder() //
.ifNull(Fields.field("optional")) //
.thenReplaceWith(Fields.field("never-null"));
DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT);
BasicDBList expected = new BasicDbListBuilder() //
.add("$optional") //
.add("$never-null")//
.get();
assertThat(dbObject, isBsonObject().containing("$ifNull", expected));
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.core.aggregation;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.aggregation.Fields.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -89,7 +91,7 @@ public class TypeBasedAggregationOperationContextUnitTests {
AggregationOperationContext context = getContext(Foo.class); AggregationOperationContext context = getContext(Foo.class);
Field field = Fields.field("bar.name"); Field field = field("bar.name");
assertThat(context.getReference("bar.name"), is(notNullValue())); assertThat(context.getReference("bar.name"), is(notNullValue()));
assertThat(context.getReference(field), is(notNullValue())); assertThat(context.getReference(field), is(notNullValue()));
@@ -103,7 +105,7 @@ public class TypeBasedAggregationOperationContextUnitTests {
public void aliasesIdFieldCorrectly() { public void aliasesIdFieldCorrectly() {
AggregationOperationContext context = getContext(Foo.class); AggregationOperationContext context = getContext(Foo.class);
assertThat(context.getReference("id"), is(new FieldReference(new ExposedField(Fields.field("id", "_id"), true)))); assertThat(context.getReference("id"), is(new FieldReference(new ExposedField(field("id", "_id"), true))));
} }
/** /**
@@ -279,6 +281,58 @@ public class TypeBasedAggregationOperationContextUnitTests {
agg.toDbObject("meterData", context); agg.toDbObject("meterData", context);
} }
/**
* @see DATAMONGO-861
*/
@Test
public void rendersAggregationConditionalInTypedAggregationContextCorrectly() {
AggregationOperationContext context = getContext(FooPerson.class);
TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class,
project("name") //
.and("age") //
.transform(conditional(Criteria.where("age.value").lt(10), new Age(0), field("age"))) //
);
DBObject dbo = agg.toDbObject("person", context);
DBObject projection = getPipelineElementFromAggregationAt(dbo, 0);
assertThat(projection.containsField("$project"), is(true));
DBObject project = getValue(projection, "$project");
DBObject age = getValue(project, "age");
assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("then.value", 0));
assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("then._class", Age.class.getName()));
assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("else", "$age"));
}
/**
* @see DATAMONGO-861
*/
@Test
public void rendersAggregationIfNullInTypedAggregationContextCorrectly() {
AggregationOperationContext context = getContext(FooPerson.class);
TypedAggregation<FooPerson> agg = newAggregation(FooPerson.class,
project("name") //
.and("age") //
.transform(ifNull("age", new Age(0))) //
);
DBObject dbo = agg.toDbObject("person", context);
DBObject projection = getPipelineElementFromAggregationAt(dbo, 0);
assertThat(projection.containsField("$project"), is(true));
DBObject project = getValue(projection, "$project");
DBObject age = getValue(project, "age");
assertThat(age, isBsonObject().containing("$ifNull.[0]", "$age"));
assertThat(age, isBsonObject().containing("$ifNull.[1].value", 0));
assertThat(age, isBsonObject().containing("$ifNull.[1]._class", Age.class.getName()));
}
@Document(collection = "person") @Document(collection = "person")
public static class FooPerson { public static class FooPerson {

View File

@@ -4,6 +4,7 @@
[[new-features.1-10-0]] [[new-features.1-10-0]]
== What's new in Spring Data MongoDB 1.10 == What's new in Spring Data MongoDB 1.10
* Support for `$min`, `$max` and `$slice` operators via `Update`. * Support for `$min`, `$max` and `$slice` operators via `Update`.
* Support for `$cond` and `$ifNull` operators via `Aggregation`.
[[new-features.1-9-0]] [[new-features.1-9-0]]
== What's new in Spring Data MongoDB 1.9 == What's new in Spring Data MongoDB 1.9

View File

@@ -1688,6 +1688,9 @@ At the time of this writing we provide support for the following Aggregation Ope
| Array Aggregation Operators | Array Aggregation Operators
| size, slice | size, slice
| Conditional Aggregation Operators
| cond, ifNull
|=== |===
Note that the aggregation operations not listed here are currently not supported by Spring Data MongoDB. Comparison aggregation operators are expressed as `Criteria` expressions. Note that the aggregation operations not listed here are currently not supported by Spring Data MongoDB. Comparison aggregation operators are expressed as `Criteria` expressions.
@@ -1985,6 +1988,50 @@ List<DBObject> resultList = result.getMappedResults();
Note that we can also refer to other fields of the document within the SpEL expression. Note that we can also refer to other fields of the document within the SpEL expression.
[[mongo.aggregation.examples.example6]]
.Aggregation Framework Example 6
This example uses conditional projection. It's derived from the https://docs.mongodb.com/manual/reference/operator/aggregation/cond/[$cond reference documentation].
[source,java]
----
public class InventoryItem {
@Id int id;
String item;
String description;
int qty;
}
public class InventoryItemProjection {
@Id int id;
String item;
String description;
int qty;
int discount
}
----
[source,java]
----
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
TypedAggregation<InventoryItem> agg = newAggregation(InventoryItem.class,
project("item").and("discount")
.transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250))
.then(30)
.otherwise(20))
.and(ifNull("description", "Unspecified")).as("description")
);
AggregationResults<InventoryItemProjection> result = mongoTemplate.aggregate(agg, "inventory", InventoryItemProjection.class);
List<InventoryItemProjection> stateStatsList = result.getMappedResults();
----
* This one-step aggregation uses a projection operation with the `inventory` collection. We project the `discount` field using a conditional transformation for all inventory items that have a `qty` greater or equal to `250`. An second conditional projection is performed for the `description` field. We apply the description `Unspecified` to all items that either do not have a `description` field of items that have a `null` description.
[[mongo.custom-converters]] [[mongo.custom-converters]]
== Overriding default mapping with custom converters == Overriding default mapping with custom converters