聚合框架支持
Spring Data MongoDB 为 MongoDB 2.2 版本中引入的聚合框架提供支持。
有关更多信息,请参阅聚合框架的完整 参考文档 以及 MongoDB 的其他数据聚合工具。
基本概念
Spring Data MongoDB 中的聚合框架支持基于以下关键抽象:Aggregation
、AggregationDefinition
和 AggregationResults
。
-
聚合
一个
Aggregation
代表一个MongoDBaggregate
操作,并包含聚合管道指令的描述。聚合通过调用Aggregation
类的相应newAggregation(…)
静态工厂方法创建,该方法接受一个AggregateOperation
列表和一个可选的输入类。实际的聚合操作由
MongoTemplate
的aggregate
方法运行,该方法将所需的输出类作为参数。 -
TypedAggregation
一个
TypedAggregation
,就像一个Aggregation
一样,包含聚合管道的指令和对输入类型的引用,该引用用于将域属性映射到实际的文档字段。在运行时,字段引用会根据给定的输入类型进行检查,并考虑潜在的
@Field
注释。
在 3.2 中更改,引用不存在的属性不再引发错误。要恢复之前的行为,请使用AggregationOptions
的strictMapping
选项。
-
AggregationDefinition
一个
AggregationDefinition
代表一个MongoDB聚合管道操作,并描述了在此聚合步骤中应执行的处理。虽然您可以手动创建一个AggregationDefinition
,但我们建议使用Aggregate
类提供的静态工厂方法来构造一个AggregateOperation
。 -
AggregationResults
AggregationResults
是聚合操作结果的容器。它提供对原始聚合结果的访问,以Document
形式访问映射的对象和其他有关聚合的信息。以下列表显示了使用 Spring Data MongoDB 支持 MongoDB 聚合框架的典型示例
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; 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 聚合框架提供以下类型的聚合阶段和操作
-
addFields -
AddFieldsOperation
-
bucket / bucketAuto -
BucketOperation
/BucketAutoOperation
-
count -
CountOperation
-
densify -
DensifyOperation
-
facet -
FacetOperation
-
geoNear -
GeoNearOperation
-
graphLookup -
GraphLookupOperation
-
group -
GroupOperation
-
limit -
LimitOperation
-
lookup -
LookupOperation
-
match -
MatchOperation
-
merge -
MergeOperation
-
project -
ProjectionOperation
-
redact -
RedactOperation
-
replaceRoot -
ReplaceRootOperation
-
sample -
SampleOperation
-
set -
SetOperation
-
setWindowFields -
SetWindowFieldsOperation
-
skip -
SkipOperation
-
sort / sortByCount -
SortOperation
/SortByCountOperation
-
unionWith -
UnionWithOperation
-
unset -
UnsetOperation
-
unwind -
UnwindOperation
不支持的聚合阶段(例如 MongoDB Atlas 的 $search)可以通过实现
|
在撰写本文时,我们为 Spring Data MongoDB 提供了对以下聚合运算符的支持
集合聚合运算符 |
|
分组/累加器聚合运算符 |
|
算术聚合运算符 |
|
字符串聚合运算符 |
|
比较聚合运算符 |
|
数组聚合运算符 |
|
文字运算符 |
|
日期聚合运算符 |
|
变量运算符 |
|
条件聚合运算符 |
|
类型聚合运算符 |
|
转换聚合运算符 |
|
对象聚合运算符 |
|
脚本聚合运算符 |
|
* 该操作由 Spring Data MongoDB 映射或添加。
请注意,此处未列出的聚合操作目前不受 Spring Data MongoDB 支持。比较聚合运算符表示为 Criteria
表达式。
投影表达式
投影表达式用于定义特定聚合步骤结果的字段。投影表达式可以通过 Aggregation
类的 project
方法定义,方法是传递 String
对象列表或聚合框架 Fields
对象。可以通过使用 and(String)
方法的流畅 API 扩展投影以添加其他字段,并通过使用 as(String)
方法进行别名化。请注意,您还可以使用聚合框架的 Fields.field
静态工厂方法定义带有别名的字段,然后可以使用该方法构造新的 Fields
实例。在后面的聚合阶段中对投影字段的引用仅对包含字段的字段名或其别名(包括新定义的字段及其别名)有效。在后面的聚合阶段中无法引用未包含在投影中的字段。以下列表显示了投影表达式的示例
// 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")
// 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 聚合框架参考文档的相应部分。
分面分类
从 3.4 版本开始,MongoDB 支持使用聚合框架进行分面分类。分面分类使用语义类别(通用或特定主题)来组合创建完整的分类条目。流经聚合管道的文档被分类到桶中。多方面分类允许对同一组输入文档进行各种聚合,而无需多次检索输入文档。
桶
桶操作根据指定的表达式和桶边界将传入的文档分类到称为桶的组中。桶操作需要一个分组字段或一个分组表达式。您可以使用 `Aggregate` 类的 `bucket()` 和 `bucketAuto()` 方法来定义它们。`BucketOperation` 和 `BucketAutoOperation` 可以根据输入文档的聚合表达式公开累积。您可以通过使用 `with…()` 方法和 `andOutput(String)` 方法,通过流畅的 API 使用其他参数扩展桶操作。您可以使用 `as(String)` 方法为操作添加别名。每个桶在输出中表示为一个文档。
BucketOperation
使用一组定义的边界将传入的文档分组到这些类别中。边界需要排序。以下列表显示了一些桶操作示例
// 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
尝试将文档均匀分布到指定数量的桶中,以确定边界。BucketAutoOperation
可选地接受一个粒度值,该值指定要使用的 首选数字 系列,以确保计算出的边界边缘以首选的圆形数字或 10 的幂结束。以下列表显示了桶操作示例
// 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");
为了在桶中创建输出字段,桶操作可以使用 `AggregationExpression` 通过 `andOutput()` 和 SpEL 表达式 通过 `andOutputExpression()`。
请注意,有关桶表达式的更多详细信息可以在 MongoDB 聚合框架参考文档的 $bucket
部分 和 $bucketAuto
部分 中找到。
多方面聚合
可以使用多个聚合管道来创建多方面聚合,这些聚合在单个聚合阶段内跨多个维度(或方面)表征数据。多方面聚合提供多个过滤器和分类,以指导数据浏览和分析。分面的常见实现是许多在线零售商如何提供通过对产品价格、制造商、尺寸和其他因素应用过滤器来缩小搜索结果范围的方法。
您可以使用 Aggregation
类的 facet()
方法定义 FacetOperation
。您可以使用 and()
方法通过多个聚合管道对其进行自定义。每个子管道在输出文档中都有自己的字段,其结果存储为文档数组。
子管道可以在分组之前对输入文档进行投影和过滤。常见的用例包括在分类之前提取日期部分或进行计算。以下清单显示了方面操作示例
// 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"))
请注意,有关方面操作的更多详细信息,请参阅 MongoDB 聚合框架参考文档的 $facet
部分。
按计数排序
按计数排序操作根据指定表达式的值对传入的文档进行分组,计算每个不同组中的文档计数,并按计数对结果进行排序。它提供了一个方便的快捷方式,可以在使用 分面分类 时应用排序。按计数排序操作需要一个分组字段或分组表达式。以下清单显示了按计数排序的示例
// generates { $sortByCount: "$country" }
sortByCount("country");
按计数排序操作等效于以下 BSON(二进制 JSON)
{ $group: { _id: <expression>, count: { $sum: 1 } } }, { $sort: { count: -1 } }
投影表达式中的 Spring 表达式支持
我们通过 ProjectionOperation
和 BucketOperation
类的 andExpression
方法支持在投影表达式中使用 SpEL 表达式。此功能允许您将所需表达式定义为 SpEL 表达式。在运行查询时,SpEL 表达式将转换为相应的 MongoDB 投影表达式部分。这种安排使表达复杂的计算变得容易得多。
使用 SpEL 表达式的复杂计算
考虑以下 SpEL 表达式
1 + (q + 1) / (q - 1)
前面的表达式将转换为以下投影表达式部分
{ "$add" : [ 1, {
"$divide" : [ {
"$add":["$q", 1]}, {
"$subtract":[ "$q", 1]}
]
}]}
您可以在 聚合框架示例 5 和 聚合框架示例 6 中看到更多上下文中的示例。您可以在 SpelExpressionTransformerUnitTests
中找到支持的 SpEL 表达式构造的更多使用示例。
支持的 SpEL 转换
SpEL 表达式 | Mongo 表达式部分 |
---|---|
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})");
聚合框架示例
本节中的示例演示了 Spring Data MongoDB 与 MongoDB 聚合框架的用法模式。
聚合框架示例 1
在这个入门示例中,我们希望聚合一个标签列表,以获取 MongoDB 集合(称为 tags
)中特定标签的出现次数,并按出现次数降序排序。此示例演示了分组、排序、投影(选择)和解包(结果拆分)的用法。
class TagCount {
String tag;
int n;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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();
前面的列表使用以下算法
-
使用
newAggregation
静态工厂方法创建一个新的聚合,我们将聚合操作列表传递给它。这些聚合操作定义了我们Aggregation
的聚合管道。 -
使用
project
操作从输入集合中选择tags
字段(它是一个字符串数组)。 -
使用
unwind
操作为tags
数组中的每个标签生成一个新文档。 -
使用
group
操作为每个tags
值定义一个组,我们为其聚合出现次数(使用count
聚合运算符并将结果收集到一个名为n
的新字段中)。 -
选择
n
字段并为从上一个分组操作生成的 ID 字段创建一个别名(因此调用previousOperation()
),其名称为tag
。 -
使用
sort
操作按出现次数降序对生成的标签列表进行排序。 -
在
MongoTemplate
上调用aggregate
方法,让 MongoDB 执行实际的聚合操作,并将创建的Aggregation
作为参数。
请注意,输入集合被显式地指定为aggregate
方法的tags
参数。如果输入集合的名称没有被显式指定,它将从作为newAggreation
方法的第一个参数传递的输入类中推断出来。
聚合框架示例 2
此示例基于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;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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
类定义了所需输出格式的结构。
前面的列表使用以下算法
-
使用
group
操作从输入集合中定义一个组。分组标准是state
和city
字段的组合,它形成了组的ID结构。我们使用sum
运算符聚合来自分组元素的population
属性的值,并将结果保存在pop
字段中。 -
使用
sort
操作按pop
、state
和city
字段升序对中间结果进行排序,这样最小的城市位于顶部,最大的城市位于结果的底部。请注意,对state
和city
的排序是隐式地针对组ID字段执行的(Spring Data MongoDB处理了这一点)。 -
再次使用
group
操作按state
对中间结果进行分组。请注意,state
再次隐式地引用组ID字段。我们使用last(…)
和first(…)
运算符分别在project
操作中选择最大和最小城市的名称和人口计数。 -
从之前的
group
操作中选择state
字段。请注意,state
再次隐式地引用组ID字段。因为我们不希望出现隐式生成的ID,所以我们使用and(previousOperation()).exclude()
从之前的操作中排除ID。因为我们希望在输出类中填充嵌套的City
结构,所以我们必须使用嵌套方法发出适当的子文档。 -
在
sort
操作中,按升序对StateStats
列表的州名称进行排序。
请注意,我们从作为 newAggregation
方法第一个参数传递的 ZipInfo
类中推导出输入集合的名称。
聚合框架示例 3
此示例基于 MongoDB 聚合框架文档中的 人口超过 1000 万的州 示例。我们添加了额外的排序以在不同的 MongoDB 版本中产生稳定的结果。在这里,我们希望使用聚合框架返回所有人口超过 1000 万的州。此示例演示了分组、排序和匹配(过滤)。
class StateStats {
@Id String id;
String state;
@Field("totalPop") int totalPopulation;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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();
前面的列表使用以下算法
-
按
state
字段对输入集合进行分组,并计算population
字段的总和,并将结果存储在新字段"totalPop"
中。 -
除了按升序对
"totalPop"
字段进行排序外,还按先前分组操作的 id 引用对中间结果进行排序。 -
使用
match
操作对中间结果进行过滤,该操作接受Criteria
查询作为参数。
请注意,我们从作为 newAggregation
方法第一个参数传递的 ZipInfo
类中推导出输入集合的名称。
聚合框架示例 4
此示例演示了在投影操作中使用简单的算术运算。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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
类中推导出输入集合的名称。
聚合框架示例 5
此示例演示了在投影操作中使用从 SpEL 表达式派生的简单算术运算。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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();
聚合框架示例 6
此示例演示了在投影操作中使用从 SpEL 表达式派生的复杂算术运算。
注意:传递给 addExpression
方法的附加参数可以使用索引器表达式根据其位置进行引用。在此示例中,我们使用 [0]
引用参数数组的第一个参数。当 SpEL 表达式转换为 MongoDB 聚合框架表达式时,外部参数表达式将被其相应的值替换。
class Product {
String id;
String name;
double netPrice;
int spaceUnits;
}
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
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 表达式中引用文档的其他字段。
聚合框架示例 7
此示例使用条件投影。它源自 $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
}
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();
此一步聚合使用带有inventory
集合的投影操作。对于所有qty
大于或等于250
的库存项,我们使用条件操作投影discount
字段。对description
字段执行第二个条件投影。我们将Unspecified
描述应用于所有没有description
字段的项目或description
字段为null
的项目。
从 MongoDB 3.6 开始,可以使用条件表达式从投影中排除字段。
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 的字段值。 |