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);
}
/**
* 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.
*

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) {
List<Projection> projections = this.projections.isEmpty() ? Collections.<Projection> emptyList() : this.projections
.subList(0, this.projections.size() - 1);
List<Projection> projections = this.projections.isEmpty() ? Collections.<Projection> emptyList()
: this.projections.subList(0, this.projections.size() - 1);
return new ProjectionOperation(projections, Arrays.asList(projection));
}
@@ -240,6 +240,24 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return
*/
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 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);
this.name = name;
@@ -419,7 +438,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
/**
* Allows to specify an alias for the previous projection operation.
*
* @param string
* @param alias
* @return
*/
@Override
@@ -436,6 +455,24 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
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.
*
@@ -764,7 +801,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
this.values = Arrays.asList(values);
}
/*
/*
* (non-Javadoc)
* @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.springframework.data.domain.Sort.Direction.*;
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.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.aggregation.AggregationTests.CarDescriptor.Entry;
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.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
@@ -131,6 +133,8 @@ public class AggregationTests {
mongoTemplate.dropCollection(Reservation.class);
mongoTemplate.dropCollection(Venue.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));
}
/**
* @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
*/
@@ -1460,4 +1708,31 @@ public class AggregationTests {
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.springframework.data.mongodb.core.DBObjectTestUtils.*;
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.test.util.IsBsonObject.*;
@@ -29,7 +30,10 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
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.BasicDBObjectBuilder;
import com.mongodb.DBObject;
@@ -243,6 +247,25 @@ public class AggregationUnitTests {
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
*/
@@ -379,6 +402,160 @@ public class AggregationUnitTests {
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) {
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.junit.Assert.*;
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.List;
@@ -89,7 +91,7 @@ public class TypeBasedAggregationOperationContextUnitTests {
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(field), is(notNullValue()));
@@ -103,7 +105,7 @@ public class TypeBasedAggregationOperationContextUnitTests {
public void aliasesIdFieldCorrectly() {
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);
}
/**
* @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")
public static class FooPerson {

View File

@@ -4,6 +4,7 @@
[[new-features.1-10-0]]
== What's new in Spring Data MongoDB 1.10
* Support for `$min`, `$max` and `$slice` operators via `Update`.
* Support for `$cond` and `$ifNull` operators via `Aggregation`.
[[new-features.1-9-0]]
== 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
| 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.
@@ -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.
[[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]]
== Overriding default mapping with custom converters