Aggregation Framework Support
Spring Data MongoDB 通过 Aggregation 类提供对聚合框架的支持,该类可用于创建和执行聚合管道。它还提供了一系列聚合操作符,这些操作符映射到 MongoDB 聚合框架中的操作。
Spring Data MongoDB 还支持分面聚合,它允许您创建多维度的聚合,其中数据可以跨多个维度进行分类和分析。
此外,Spring Data MongoDB 提供对投影表达式中 SpEL 表达式的支持,允许您使用简单的算术运算和复杂的计算来定义投影字段。
Spring Data MongoDB 为 MongoDB 2.2 版中引入的聚合框架提供支持。 有关更多信息,请参阅聚合框架的完整 reference documentation 和 MongoDB 的其他数据聚合工具。
Basic Concepts
Spring Data MongoDB 中的聚合框架支持基于以下主要抽象:“聚合”、“聚合定义”和“聚合结果”。
-
Aggregation
“聚合”表示 MongoDB“聚合”操作,并保存聚合管道指令的描述。聚合是通过调用Aggregation
类的相应newAggregation(…)
静态工厂方法创建的,该方法采用AggregateOperation
列表和可选输入类。 实际的聚合操作由MongoTemplate
的aggregate
方法运行,该方法将所需输出类作为参数。 -
TypedAggregation
“类型化聚合”与“聚合”一样,保存聚合管道的指令,并引用输入类型,该输入类型用于将域属性映射到实际文档字段。 在运行时,针对所给输入类型检查字段引用,同时考虑潜在的@Field
注释。
更改于 3.2 中,现在引用非存在属性不再产生错误。若要恢复先前行为,请使用 AggregationOptions
的 strictMapping
选项。
-
AggregationDefinition
“聚合定义”表示 MongoDB 聚合管道操作,并描述在此聚合步骤中应执行的处理。虽然您可以手动创建AggregationDefinition
,但我们建议使用Aggregate
类提供的静态工厂方法来构建AggregateOperation
。 -
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
可以通过实现
|
在撰写本文时,我们为 Spring Data MongoDB 中的以下聚合操作员提供了支持:
Set Aggregation Operators |
|
Group/Accumulator Aggregation Operators |
|
Arithmetic Aggregation Operators |
|
String Aggregation Operators |
|
Comparison Aggregation Operators |
|
Array Aggregation Operators |
|
Literal Operators |
|
Date Aggregation Operators |
|
Variable Operators |
|
Conditional Aggregation Operators |
|
Type Aggregation Operators |
|
Convert Aggregation Operators |
|
Object Aggregation Operators |
|
Script Aggregation Operators |
|
-
此操作由 Spring Data MongoDB 映射或添加。
请注意,此处未列出的聚合操作目前不受 Spring Data MongoDB 支持。比较聚合操作符表示为 Criteria
表达式。
Projection Expressions
投影表达式用于定义特定聚合步骤的结果字段。投影表达式可以通过 Aggregation
类的 project
方法来定义,方法是传递一系列 String
对象或聚合框架 Fields
对象。可以通过使用 and(String)
方法和使用 as(String)
方法设置别名来通过流利 API 扩展投影的其他字段。请注意,您还可以使用聚合框架的 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 聚合框架参考文档 corresponding section 中找到有关投影表达式的更多详细信息。
Faceted Classification
3.4 版及更高版本,MongoDB 使用聚合框架支持多方面分类。多方面分类使用语义类别(通用或特定于主题)组合来创建完整的分类项。流经聚合管道的文档被分类为各种类别。多方面分类能够在同一组输入文档上启用各种聚合,而无需多次检索输入文档。
Buckets
基于指定的表达式和桶边界,桶操作将传入的文档分类为组(称为桶)。桶操作需要一个分组字段或分组表达式。可以使用 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
可选采用一个粒度值,指定 preferred number 序列的使用,以确保计算的边界边缘以较优的圆整数字或 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 expressions 到 andOutputExpression()
。
请注意,可以在 MongoDB 聚合框架参考文档的 $bucket
section 和 $bucketAuto
section 中找到有关存储段表达式的更多详细信息。
Multi-faceted Aggregation
可以使用多个聚合管道来创建多方面的聚合,它们可以在单个聚合阶段中表征多个维度(或方面)中的数据。多方面的聚合提供了多个过滤器和分类以指导数据浏览和分析。分面的一个常见实现是许多在线零售商如何通过对产品价格、制造商、尺寸和其他因素应用过滤器来缩小搜索结果范围。
可以使用 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
section 中找到有关方面操作的更多详细信息。
Sort By Count
按计数排序操作根据指定表达式的值对传入的文档进行分组,计算每个不同组中文档的计数,并按计数对结果进行排序。它提供了一个在使用 Faceted Classification 时应用排序的便捷快捷方式。按计数排序操作需要一个分组字段或分组表达式。以下列表显示按计数排序示例:
// generates { $sortByCount: "$country" }
sortByCount("country");
按计数排序操作等效于以下 BSON(二进制 JSON):
{ $group: { _id: <expression>, count: { $sum: 1 } } }, { $sort: { count: -1 } }
Spring Expression Support in Projection Expressions
我们通过 ProjectionOperation
和 BucketOperation
类的 andExpression
方法支持在投影表达式中使用 SpEL 表达式。此功能允许您将所需的表达式定义为 SpEL 表达式。在运行查询时,SpEL 表达式将转换为相应的 MongoDB 投影表达式部分。这种安排使得表达复杂的计算变得更加容易。
Complex Calculations with SpEL expressions
考虑以下 SpEL 表达式:
1 + (q + 1) / (q - 1)
前述表达式转换为以下投影表达式部分:
{ "$add" : [ 1, {
"$divide" : [ {
"$add":["$q", 1]}, {
"$subtract":[ "$q", 1]}
]
}]}
您可以在 Aggregation Framework Example 5 和 Aggregation Framework Example 6 中看到更多上下文示例。您可以在 SpelExpressionTransformerUnitTests
中找到更多受支持的 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})");
Aggregation Framework Examples
本节中的示例演示了 MongoDB 聚合框架与 Spring Data MongoDB 的用法模式。
Aggregation Framework Example 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
操作为我们聚合出现次数(使用count
聚合运算符并将结果收集到一个名为n
的新字段中)的每个tags
值定义一个组。 -
选择
n
字段,并为先前组操作生成的 ID 字段创建别名为tag
的别名(因此调用previousOperation()
)。 -
使用
sort
操作按出现次数降序对标签结果列表进行排序。 -
在
MongoTemplate
上调用aggregate
方法,让 MongoDB 使用创建的Aggregation
作为参数执行实际的聚合操作。
请注意,输入集合已被明确指定为 aggregate
方法的 tags
参数。如果输入集合的名称未明确指定,则它将派生自作为 newAggreation
方法的第一个参数传递的输入类。
Aggregation Framework Example 2
此示例基于 MongoDB 聚合框架文档中的 Largest and Smallest Cities by State 示例。我们添加了额外的排序以生成在不同 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 字段。我们在project
操作中分别使用last(…)
和first(…​)
运算符调用,选择最大和最小的城市的名和人口数。 -
从上一个
group
操作中选择state
字段。请注意,state
再次隐式引用组 ID 字段。由于我们不希望显示一个隐式生成的 ID,因此我们通过使用and(previousOperation()).exclude()
从上一个操作中排除 ID。由于我们希望填充输出类中的嵌套City
结构,因此我们必须使用嵌套方法发出适当的子文档。 -
在
sort
操作中按升序对StateStats
的结果列表按其州名称进行排序。
请注意,我们从作为 newAggregation
方法的第一个参数传递的 ZipInfo
类派生输入集合的名称。
Aggregation Framework Example 3
此示例基于 MongoDB 聚合框架文档中的 States with Populations Over 10 Million 示例。我们添加了额外的排序以生成在不同 MongoDB 版本中具有稳定性的结果。在这里,我们希望通过使用聚合框架,返回人口超过 1 千万的所有州。此示例演示了分组、排序和匹配(筛选)。
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"
字段中。 -
按上一个组操作的 ID 引用以及
"totalPop"
字段按升序对中间结果进行排序。 -
使用
match
操作(它接受Criteria
查询作为参数)对中间结果进行过滤。
请注意,我们从作为 newAggregation
方法的第一个参数传递的 ZipInfo
类派生输入集合的名称。
Aggregation Framework Example 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
类派生输入集合的名称。
Aggregation Framework Example 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();
Aggregation Framework Example 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 表达式中引用文档的其他字段。
Aggregation Framework Example 7
此示例使用条件投影。它派生自 $cond reference documentation。
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
字段又有 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 的字段值。 |