Spring Data MongoDB中的聚合框架支持基于以下关键抽象:Aggregation、AggregationDefinition和AggregationResults。
在3.2版本中做出更改,引用不存在的属性不再引发错误。要恢复以前的行为,请使用AggregationOptions的strictMapping选项。
以下列表展示了使用Spring Data MongoDB对MongoDB聚合框架的支持的典型示例:
Aggregation agg = newAggregation(
pipelineOP1(),
pipelineOP2(),
pipelineOPn()
);
AggregationResults<OutputType> results = mongoTemplate.aggregate(agg, "INPUT_COLLECTION_NAME", OutputType.class);
List<OutputType> mappedResult = results.getMappedResults();
注意,如果你提供一个输入类作为newAggregation方法的第一个参数,那么MongoTemplate将从该类派生输入集合的名称。否则,如果不指定输入类,则必须显式地提供输入集合的名称。如果同时提供了输入类和输入集合,则后者优先。
支持的聚合操作和阶段
MongoDB聚合框架提供了以下类型的聚合阶段和操作:
不支持的聚合阶段(如MongoDB Atlas的$search)可以通过实现AggregationOperation或Bson表示来提供。Aggregation.stage是通过提供其JSON来注册pipeline stage的快捷方式。
Aggregation.stage("""
{ $search : {
"near": {
"path": "released",
"origin": { "$date": { "$numberLong": "..." } } ,
"pivot": 7
}
}
}
""");
在撰写本文时,Spring Data MongoDB为以下聚合运算符提供支持:
表1:Spring Data MongoDB当前支持的聚合运算符
分类 | 运算符 |
---|---|
Set Aggregation Operators | setEquals, setIntersection, setUnion, setDifference, setIsSubset, anyElementTrue, allElementsTrue |
Group/Accumulator Aggregation Operators | addToSet, bottom, bottomN, covariancePop, covarianceSamp, expMovingAvg, first, firstN, last, lastN max, maxN, min, minN, avg, push, sum, top, topN, count (*), median, percentile, stdDevPop, stdDevSamp |
Arithmetic Aggregation Operators | abs, acos, acosh, add (* via plus), asin, asin, atan, atan2, atanh, ceil, cos, cosh, derivative, divide, exp, floor, integral, ln, log, log10, mod, multiply, pow, round, sqrt, subtract (* via minus), sin, sinh, tan, tanh, trunc |
String Aggregation Operators | concat, substr, toLower, toUpper, strcasecmp, indexOfBytes, indexOfCP, regexFind, regexFindAll, regexMatch, replaceAll, replaceOne, split`, strLenBytes, strLenCP, substrCP, trim, ltrim, rtim |
Comparison Aggregation Operators | eq (* via is), gt, gte, lt, lte, ne |
Array Aggregation Operators | arrayElementAt, arrayToObject, concatArrays, filter, first, in, indexOfArray, isArray, last, range`, reverseArray, reduce, size, sortArray, slice, zip |
Literal Operators | literal |
Date Aggregation Operators | dateSubstract, dateTrunc, dayOfYear, dayOfMonth, dayOfWeek, year, month, week, hour, minute, second, millisecond, dateAdd, dateDiff, dateToString, dateFromString, dateFromParts, dateToParts, isoDayOfWeek, isoWeek, isoWeekYear, tsIncrement, tsSecond |
Variable Operators | map |
Conditional Aggregation Operators | cond, ifNull, switch |
Type Aggregation Operators | type |
Convert Aggregation Operators | convert, degreesToRadians, toBool, toDate, toDecimal, toDouble, toInt, toLong, toObjectId, toString |
Object Aggregation Operators | objectToArray, mergeObjects, getField, setField |
Script Aggregation Operators | function, accumulator |
*操作由Spring Data MongoDB映射或添加。
请注意,此处未列出的聚合操作目前不被Spring Data MongoDB支持。比较聚合运算符表示为Criteria表达式。
投影表达式用于定义特定聚合步骤的结果字段。投影表达式可以通过Aggregation类的project方法定义,也可以通过传递String对象列表或聚合框架Fields对象来定义。可以使用and(String)方法通过fluent API使用附加字段扩展投影,并使用as(String)方法对其起别名。请注意,你还可以使用聚合框架的Fields.field静态工厂方法定义具有别名的字段,然后可以使用该方法构造新的Fields实例。在后面的聚合阶段中对投影字段的引用仅对包含字段的字段名或其别名(包括新定义的字段及其别名)有效。未包含在投影中的字段不能在以后的聚合阶段中引用。以下列表展示了投影表达式的示例:
例1:投影表达式示例
// generates {$project: {name: 1, netPrice: 1}}
project("name", "netPrice")
// generates {$project: {thing1: $thing2}}
project().and("thing1").as("thing2")
// generates {$project: {a: 1, b: 1, thing2: $thing1}}
project("a","b").and("thing1").as("thing2")
例2。使用投影和排序的多阶段聚合
// generates {$project: {name: 1, netPrice: 1}}, {$sort: {name: 1}}
project("name", "netPrice"), sort(ASC, "name")
// generates {$project: {name: $firstname}}, {$sort: {name: 1}}
project().and("firstname").as("name"), sort(ASC, "name")
// does not work
project().and("firstname").as("name"), sort(ASC, "firstname")
项目操作的更多示例可以在AggregationTests类中找到。请注意,关于投影表达式的更多细节可以在MongoDB Aggregation Framework参考文档的相应部分中找到。
从3.4版本开始,MongoDB使用聚合框架支持分面分类(faceted classification)。分面分类使用语义类别(通用或特定主题),这些类别组合在一起创建完整的分类条目。流经聚合管道的Documents被分类到存储桶中。多方面分类允许对同一组输入documents进行各种聚合,而无需多次检索输入documents。
Bucket操作根据指定的表达式和bucket边界将传入documents分类为组,称为buckets。Bucket操作需要分组字段或分组表达式。你可以使用Aggregate类的bucket()和bucketAuto()方法来定义它们。BucketOperation和BucketAutoOperation可以公开基于输入文档的聚合表达式的累积(accumulations)。你可以使用with…()方法和andOutput(String)方法,通过fluent API使用附加参数来扩展bucket操作。你可以使用as(String)方法对操作起别名。每个bucket在输出中表示为一个document。
BucketOperation采用一组定义的边界将传入documents分组到这些类别中。需要对边界进行排序。以下列表展示了bucket操作的一些示例:
例3:Bucket操作示例
// generates {$bucket: {groupBy: $price, boundaries: [0, 100, 400]}}
bucket("price").withBoundaries(0, 100, 400);
// generates {$bucket: {groupBy: $price, default: "Other" boundaries: [0, 100]}}
bucket("price").withBoundaries(0, 100).withDefault("Other");
// generates {$bucket: {groupBy: $price, boundaries: [0, 100], output: { count: { $sum: 1}}}}
bucket("price").withBoundaries(0, 100).andOutputCount().as("count");
// generates {$bucket: {groupBy: $price, boundaries: [0, 100], 5, output: { titles: { $push: "$title"}}}
bucket("price").withBoundaries(0, 100).andOutput("title").push().as("titles");
BucketAutoOperation确定边界,试图将documents平均分配到指定数量的存储桶中。BucketAutoOperation可选地采用一个粒度值,该值指定要使用的首选数字序列,以确保计算的边界边以首选整数或10的幂结束。以下列表展示了bucket操作的示例:
例4:Bucket操作示例
// generates {$bucketAuto: {groupBy: $price, buckets: 5}}
bucketAuto("price", 5)
// generates {$bucketAuto: {groupBy: $price, buckets: 5, granularity: "E24"}}
bucketAuto("price", 5).withGranularity(Granularities.E24).withDefault("Other");
// generates {$bucketAuto: {groupBy: $price, buckets: 5, output: { titles: { $push: "$title"}}}
bucketAuto("price", 5).andOutput("title").push().as("titles");
要在buckets中创建输出字段,bucket操作可以通过andOutput()使用AggregationExpression,通过andOutputExpression()使用SpEL表达式。
请注意,有关bucket表达式的更多详细信息可以在MongoDB Aggregation Framework参考文档的$ bucket部分和$bucketAuto部分找到。
多个聚合管道可用于创建多方面聚合,这些聚合在单个聚合阶段内表征多个维度(或方面)的数据。多面聚合提供了多个过滤器和分类,以指导数据浏览和分析。faceting的一个常见实现是,有多少在线零售商通过对产品价格、制造商、尺寸和其他因素应用过滤器来缩小搜索结果的范围。
你可以使用Aggregation类的facet()方法定义FacetOperation。你还可以使用and()方法使用多个聚合管道对其进行自定义。每个子管道在输出document中都有自己的字段,其结果存储为documents数组。
子管道可以在分组之前投影和过滤输入documents。常见的用例包括在分类之前提取日期部分或进行计算。以下列表展示了方面(facet)操作示例:
例5:Facet操作示例
// generates {$facet: {categorizedByPrice: [ { $match: { price: {$exists : true}}}, { $bucketAuto: {groupBy: $price, buckets: 5}}]}}
facet(match(Criteria.where("price").exists(true)), bucketAuto("price", 5)).as("categorizedByPrice"))
// generates {$facet: {categorizedByCountry: [ { $match: { country: {$exists : true}}}, { $sortByCount: "$country"}]}}
facet(match(Criteria.where("country").exists(true)), sortByCount("country")).as("categorizedByCountry"))
// generates {$facet: {categorizedByYear: [
// { $project: { title: 1, publicationYear: { $year: "publicationDate"}}},
// { $bucketAuto: {groupBy: $price, buckets: 5, output: { titles: {$push:"$title"}}}
// ]}}
facet(project("title").and("publicationDate").extractYear().as("publicationYear"),
bucketAuto("publicationYear", 5).andOutput("title").push().as("titles"))
.as("categorizedByYear"))
请注意,关于facet操作的更多细节可以在MongoDB Aggregation Framework参考文档的$facet部分中找到。
按计数排序操作根据指定表达式的值对传入documents进行分组,计算每个distinct组中的documents数,并按计数对结果进行排序。当使用分面分类时,它提供了一个方便的快捷方式来应用排序。按计数排序操作需要分组字段或分组表达式。以下列表展示了按计数排序的示例:
例6:按计数排序示例
// generates { $sortByCount: "$country" }
sortByCount("country");
按计数排序操作相当于以下BSON(二进制JSON):
{ $group: { _id: <expression>, count: { $sum: 1 } } },
{ $sort: { count: -1 } }
通过ProjectionOperation 和BucketOperation 类的andExpression方法,框架支持在投影表达式中使用SpEL表达式。该特性允许你将所需的表达式定义为SpEL表达式。在运行查询时,SpEL表达式被转换为对应的MongoDB投影表达式部分。这种安排使表达复杂的计算变得容易很多。
参见下面的SpEL表达式:
1 + (q + 1) / (q - 1)
将上述表达式翻译成以下投影表达式部分:
{ "$add" : [ 1, {
"$divide" : [ {
"$add":["$q", 1]}, {
"$subtract":[ "$q", 1]}
]
}]}
你可以在聚合框架例5和聚合框架例6中看到更多上下文中的示例。你可以在SpelExpressionTransformerUnitTests中找到更多受支持的SpEL表达式构造的用法示例。
支持的SpEL转换
SpEL Expression | Mongo Expression Part |
---|---|
a == b | { $ eq : [$a, $b] } |
a != b | { $ ne : [$a , $b] } |
a > b | { $ gt : [$a, $b] } |
a >= b | { $ gte : [$a, $b] } |
a < b | { $ lt : [$a, $b] } |
a ⇐ b | { $ lte : [$a, $b] } |
a + b | { $ add : [$a, $b] } |
a - b | { $ subtract : [$a, $b] } |
a * b | { $ multiply : [$a, $b] } |
a / b | { $ divide : [$a, $b] } |
a^b | { $ pow : [$a, $b] } |
a % b | { $ mod : [$a, $b] } |
a && b | { $ and : [$a, $b] } |
a || b | { $or : [$a, $b] } |
!a | { $ not : [$a] } |
除了上表中展示的转换之外,你还可以使用标准的SpEL操作,例如new来(例如)创建数组并通过其名称引用表达式(后面跟着括号中要使用的参数)。下面的例子展示了如何以这种方式创建一个数组:
// { $setEquals : [$a, [5, 8, 13] ] }
.andExpression("setEquals(a, new int[]{5, 8, 13})");
本节中的示例演示了MongoDB Aggregation Framework与Spring Data MongoDB的使用模式。
在这个介绍性示例中,我们希望聚合一个标签列表,以从MongoDB集合(称为tags)中获得特定标签的出现次数,该集合按出现次数降序排序。此示例演示了分组、排序、投影(选择)和展开(结果拆分)的用法。
class TagCount {
String tag;
int n;
}
Aggregation agg = newAggregation(
project("tags"),
unwind("tags"),
group("tags").count().as("n"),
project("n").and("tag").previousOperation(),
sort(DESC, "n")
);
AggregationResults<TagCount> results = mongoTemplate.aggregate(agg, "tags", TagCount.class);
List<TagCount> tagCount = results.getMappedResults();
前面的列表使用以下算法:
请注意,输入集合被显式指定为聚合方法的tags参数。如果没有显式指定输入集合的名称,则它是从作为第一个参数传递给newAggregation方法的输入类派生的。
此示例基于MongoDB聚合框架文档中的各州最大和最小城市示例。这里添加了额外的排序,以在不同的MongoDB版本中产生稳定的结果。在这里,我们想通过使用聚合框架返回每个州按人口划分的最小和最大城市。这个例子演示了分组、排序和投影(选择)。
class ZipInfo {
String id;
String city;
String state;
@Field("pop") int population;
@Field("loc") double[] location;
}
class City {
String name;
int population;
}
class ZipInfoStats {
String id;
String state;
City biggestCity;
City smallestCity;
}
TypedAggregation<ZipInfo> aggregation = newAggregation(ZipInfo.class,
group("state", "city")
.sum("population").as("pop"),
sort(ASC, "pop", "state", "city"),
group("state")
.last("city").as("biggestCity")
.last("pop").as("biggestPop")
.first("city").as("smallestCity")
.first("pop").as("smallestPop"),
project()
.and("state").previousOperation()
.and("biggestCity")
.nested(bind("name", "biggestCity").and("population", "biggestPop"))
.and("smallestCity")
.nested(bind("name", "smallestCity").and("population", "smallestPop")),
sort(ASC, "state")
);
AggregationResults<ZipInfoStats> result = mongoTemplate.aggregate(aggregation, ZipInfoStats.class);
ZipInfoStats firstZipInfoStats = result.getMappedResults().get(0);
请注意,ZipInfo类映射给定输入集合的结构。ZipInfoStats类以所需的输出格式定义结构。
前面的列表使用以下算法:
请注意,我们从作为第一个参数传递给newAggregation方法的ZipInfo类派生输入集合的名称。
此示例基于MongoDB聚合框架文档中人口超过1000万的州的示例。我们添加了额外的排序,以在不同的MongoDB版本中产生稳定的结果。在这里,我们想使用聚合框架返回人口超过1000万的所有州。这个例子演示了分组、排序和匹配(过滤)。
class StateStats {
@Id String id;
String state;
@Field("totalPop") int totalPopulation;
}
TypedAggregation<ZipInfo> agg = newAggregation(ZipInfo.class,
group("state").sum("population").as("totalPop"),
sort(ASC, previousOperation(), "totalPop"),
match(where("totalPop").gte(10 * 1000 * 1000))
);
AggregationResults<StateStats> result = mongoTemplate.aggregate(agg, StateStats.class);
List<StateStats> stateStatsList = result.getMappedResults();
前面的列表使用以下算法:
请注意,我们从作为第一个参数传递给newAggregation方法的ZipInfo类派生输入集合的名称。
这个例子演示了在投影运算中使用简单的算术运算。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
TypedAggregation<Product> agg = newAggregation(Product.class,
project("name", "netPrice")
.and("netPrice").plus(1).as("netPricePlus1")
.and("netPrice").minus(1).as("netPriceMinus1")
.and("netPrice").multiply(1.19).as("grossPrice")
.and("netPrice").divide(2).as("netPriceDiv2")
.and("spaceUnits").mod(2).as("spaceUnitsMod2")
);
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
List<Document> resultList = result.getMappedResults();
注意,我们从作为第一个参数传递给newAggregation方法的Product类中派生输入集合的名称。
这个例子演示了在投影运算中使用源自SpEL表达式的简单算术运算。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
TypedAggregation<Product> agg = newAggregation(Product.class,
project("name", "netPrice")
.andExpression("netPrice + 1").as("netPricePlus1")
.andExpression("netPrice - 1").as("netPriceMinus1")
.andExpression("netPrice / 2").as("netPriceDiv2")
.andExpression("netPrice * 1.19").as("grossPrice")
.andExpression("spaceUnits % 2").as("spaceUnitsMod2")
.andExpression("(netPrice * 0.8 + 1.2) * 1.19").as("grossPriceIncludingDiscountAndCharge")
);
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
List<Document> resultList = result.getMappedResults();
此示例演示了在投影操作中使用从SpEL表达式派生的复杂算术运算。
注意:传递给addExpression方法的附加参数可以根据其位置与索引器表达式一起引用。在本例中,我们使用[0]引用参数数组的第一个参数。当将SpEL表达式转换为MongoDB聚合框架表达式时,外部参数表达式将替换为它们各自的值。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
double shippingCosts = 1.2;
TypedAggregation<Product> agg = newAggregation(Product.class,
project("name", "netPrice")
.andExpression("(netPrice * (1-discountRate) + [0]) * (1+taxRate)", shippingCosts).as("salesPrice")
);
AggregationResults<Document> result = mongoTemplate.aggregate(agg, Document.class);
List<Document> resultList = result.getMappedResults();
注意,我们还可以在SpEL表达式中引用document的其他字段。
这个例子使用了条件投影。它派生自$cond参考文档。
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
}
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();
此单步聚合对inventory集合使用投影操作。我们通过对数量大于或等于250的所有库存项目使用条件运算来投影折扣字段。对描述字段执行第二条件投影。我们将Unspecified描述应用于所有没有description字段的项或具有null描述的项。
从MongoDB 3.6开始,可以通过使用条件表达式从投影中排除字段。
例7:条件聚合投影
TypedAggregation<Book> agg = Aggregation.newAggregation(Book.class,
project("title")
.and(ConditionalOperators.when(ComparisonOperators.valueOf("author.middle") --------1
.equalToValue("")) --------2
.then("$$REMOVE") --------3
.otherwiseValueOf("author.middle") --------4
)
.as("author.middle"));
1. 如果字段author.middle的值
2. 不包含值,
3. 然后使用$$REMOVE排除该字段。
4. 否则,添加author.middle的字段值。