spring-data-mongodb-1.9.x中Aggregation关于Conditional Aggregation Operators的坑
截止至发布该文章前,spring-data-mongodb稳定版本还是1.9.x,本文中所提到的坑主要针对1.9.x或之前的版本,2.0.x版本的可以忽略本篇文章或直接浏览本文最后说明内容。
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和,分组等),并返回计算后的数据结果。有点类似sql语句中的 count(*),group by。MongoDB中通过在aggregate方法中添加一系列管道(pipeline)来达到数据处理的目的。(关于MongoDB aggregate的详细介绍、语法、使用说明等这里不做具体说明,大家自己查阅其他资料进行了解。)
Aggregate 中有一种操作方式是Conditional Aggregation Operators,先看看官方文档中的说明$cond (aggregation):
$cond (aggregation)
Evaluates a boolean expression to return one of the two specified return expressions.
The $cond expression has one of two syntaxes:
New in version 2.6.
{ $cond: { if: <boolean-expression>, then: <true-case>, else: <false-case-> } }
Or:
{ $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
If the <boolean-expression> evaluates to true, then $cond evaluates and returns the value of the <true-case> expression. Otherwise, $cond evaluates and returns the value of the <false-case> expression.
The arguments can be any valid expression. For more information on expressions, see Expressions.
翻译成java语法就是一个简单的三目运算符:
<boolean-expression>?<true-case>:<false-case->
再来看看官网上的例子还是上面那地址:
Example
The following example use a inventory collection with the following documents:
{ "_id" : 1, "item" : "abc1", qty: 300 }
{ "_id" : 2, "item" : "abc2", qty: 200 }
{ "_id" : 3, "item" : "xyz1", qty: 250 }
The following aggregation operation uses the $cond expression to set the discount value to 30 if qty value is greater than or equal to 250 and to 20 if qty value is less than 250:
db.inventory.aggregate(
[
{
$project:
{
item: 1,
discount:
{
$cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
}
}
}
]
)
The operation returns the following results:
{ "_id" : 1, "item" : "abc1", "discount" : 30 }
{ "_id" : 2, "item" : "abc2", "discount" : 20 }
{ "_id" : 3, "item" : "xyz1", "discount" : 30 }
The following operation uses the array syntax of the $cond expression and returns the same results:
db.inventory.aggregate(
[
{
$project:
{
item: 1,
discount:
{
$cond: [ { $gte: [ "$qty", 250 ] }, 30, 20 ]
}
}
}
]
)
例子很简单,通过$cond表达式,将qty值大于等于250的记录的discount值设为30,qty值小于250的记录的discount值设为20。
要实现上面的效果,在spring-data-mongodb中该怎么操作呢?
很遗憾,在spring-data-mongodb 1.9.x 的Reference 文档中并没有找到Conditional Aggregation Operators相关的例子,那就只能通过API中内容来找到想要的内容了。
在org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder
类中发现了这个一个方法:
public ProjectionOperation.ProjectionOperationBuilder project(String operation, Object... values)
Adds a generic projection for the current field.
Parameters:
operation - the operation key, e.g. $add.
values - the values to be set for the projection operation.
Returns:
那就试试这个方法吧,还是采用上面这个例子(代码片段,无法执行):
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
Aggregation agg = newAggregation(
project("item").and("qty").project("cond", 30, 20).as("discount")
//project("item").and("qty").project("cond", "qty >= 250", 30, 20).as("discount")
//project("item").and("qty >= 250").project("cond", 30, 20).as("discount")
//project("item").andExpression("qty >= 250").project("cond", 30, 20).as("discount")
);
mongoTemplate.aggregate(agg, "collectionName", OutputType.class);
经过尝试,发现上诉四种方式都无法达到我们想要的目的(结果不对,语法错误等)。
难道就spring-data-mongodb就无法使用$cond了吗?上个大招吧!!!
首先,自己定义一个新的AggregationOperation实现类来实现spring-data-mongodb的AggregationOperation:
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import com.mongodb.DBObject;
public class DBObjectAggregationOperation implements AggregationOperation {
private DBObject operation;
public DBObjectAggregationOperation (DBObject operation) {
this.operation = operation;
}
@Override
public DBObject toDBObject(AggregationOperationContext context) {
return context.getMappedObject(operation);
}
}
然后,我们就可以在代码中这样实现我们的功能:
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
/**
* 通过DBObject 来创建mongodb相同的project语句
* $project:
* {
* item: 1,
* iscount:
* {
* $cond: [ { $gte: [ "$qty", 250 ] }, 30, *20 ]
* }
* }
*/
DBObject operation = (DBObject) new BasicDBObject("$project", new BasicDBObject("discount",
new BasicDBObject("$cond", new Object[] {
new BasicDBObject("$gte", new Object[] {
"$qty",250
}), 30, 20 }).append("item", 1)
));
Aggregation agg = newAggregation(
new DBObjectAggregationOperation(operation)
);
mongoTemplate.aggregate(agg, "collectionName", OutputType.class);
经过测试,上诉代码能得到我们想要的结果。
mongodb的操作命令语法都是采用json(bson)格式,在处理复杂的命令时都可以通过类似上诉方法通过DBObject组装命令,然后通过spring-data-mongodb来执行得到相应的结果。
说明1:
在spring-data-mongodb 2.0.x 的Reference的文档中已经可以找到ConditionalOperator相关例子了Aggregation Framework Example 7:
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
TypedAggregation<InventoryItem> agg = newAggregation(InventoryItem.class,
project("item").and("discount")
.applyCondition(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();
github的代码中也找了相关实现类ConditionalOperator,
org.springframework.data.mongodb.core.aggregation.ConditionalOperator
后续可以通过上述例子中的代码来实现MongoDB官网的例子。