原文地址:https://www.baeldung.com/spring-data-mongodb-projections-aggregations
1. 概览
Spring Data MongoDB提供了对MongoDB原生查询语言的简单的高层抽象。在这篇文章中,我们将探索其对投影和聚合框架的支持。
如果你对这个主题不了解,请先阅读我们的介绍文章[Spring Data MongoDB简介【译者注:原文】(https://www.baeldung.com/spring-data-mongodb-tutorial)。
2. 投影
在MongoDB中,投影是一种从数据库的文档中,获取需要的字段的方式。这样会缩减很多从数据库服务器传输到客户端的数据,因此可以提高性能。
在Spring Data MongDB中,可以通过MongoTemplate和MongoRepository来使用投影。
在继续深入之前,让我们先来看下即将用到的数据模型:
@Document
public class User {
@Id
private String id;
private String name;
private Integer age;
// standard getters and setters
}
2.1 使用MongoTemplate进行投影
Field类的include()和exclude()方法用于包含或排除某个字段。
Query query = new Query();
query.fields().include("name").exclude("id");
List<User> john = mongoTemplate.find(query, User.class);
这些方法可以连在一起以包含或排除多个字段。除非显式的说明排除,用@id(数据库中的_id)标记的字段总是会显示在结果中。
当通过投影获取数据时,在结果的类实例中,被排除的字段是null。在这个例子中,字段是一个原生类型或者他们的包装类,所以这些被排除的字段的值就是原生类型的缺省值。
例如,String是null,int/Integer是0,而boolean/Boolean是false。
因此在上面的例子中,name字段是John,id是null,而age是0。
2.2 使用MongoRepository进行投影
如果使用MongoRepository,需要用Json格式定义@Query注解中的fields字段:
@Query(value="{}", fields="{name : 1, _id : 0}")
List<User> findNameAndExcludeId();
结果和使用MongoTemplate一样。value=”{}”表示没有过滤器,所以会查询到所有的文档。
3. 聚合
MongoDB中的聚合是建立在处理数据和返回计算过的结果的过程中。数据在各个环节中被处理,一个环节的输出,就是下一个环节的输入。这种在每个处理环节中应用转换和计算的能力,使得聚合成为一种非常强力的分析工具。
Spring Data MongoDB使用3个类对原生的聚合查询进行抽象。Aggregation类封装聚合查询,AggregationOperation类封装每个独立的pipeline节点,AggregationResults是聚合后产生的结果的容器。
为了实现聚合,首先使用Aggregation类的静态构造器方法创建一个聚合管道(pipeline)。然后使用Aggregation类的newAggregation()方法创建Aggregation类的实例,最后使用MongoTemplate做聚合。
MatchOperation matchStage = Aggregation.match(new Criteria("foo").is("bar"));
ProjectionOperation projectStage = Aggregation.project("foo", "bar.baz");
Aggregation aggregation
= Aggregation.newAggregation(matchStage, projectStage);
AggregationResults<OutType> output
= mongoTemplate.aggregate(aggregation, "foobar", OutType.class);
请注意,MatchOperation和ProjectionOperation都实现了* AggregationOperation*。其他聚合管道还有一些类似的实现。 OutType是期望的结果的数据模型。
现在,我们要看几个具体的例子。这些例子涵盖了主要的聚合管道和操作符。
这篇文章中使用的数据集,列出了美国所有的邮政编码,可以从这里下载到全部数据。
在test数据库中引入这个数据集,collection的名字为zips,让我们看看其中一个document。
{
"_id" : "01001",
"city" : "AGAWAM",
"loc" : [
-72.622739,
42.070206
],
"pop" : 15338,
"state" : "MA"
}
为了简化和代码整洁,下面的代码片段中,我们都假定Aggregation类的所有静态方法都已经静态的引入了。
3.1 获取所有人口大于1000万的州,并按人口数降序排列
这个例子中,我们有3个管道
- $group环节按邮政编码对人口求和
- $match环节找出那些人口超过1000万的州
- $sort环节按人口的降序排列所有的document
期望的输出会有一个_id字段代表州名字,statePop字段代表整个州的人口。让我们创建数据模型,并运行这个聚合:
public class StatePoulation {
@Id
private String state;
private Integer statePop;
// standard getters and setters
}
@id注解会把结果中的_id映射为模型中的state:
GroupOperation groupByStateAndSumPop = group("state")
.sum("pop").as("statePop");
MatchOperation filterStates = match(new Criteria("statePop").gt(10000000));
SortOperation sortByPopDesc = sort(new Sort(Direction.DESC, "statePop"));
Aggregation aggregation = newAggregation(
groupByStateAndSumPop, filterStates, sortByPopDesc);
AggregationResults<StatePopulation> result = mongoTemplate.aggregate(
aggregation, "zips", StatePopulation.class);
【此处代码有误,谨记new一个Aggregation对象时,一定要先放筛选条件,再放group部分。这是因为mongo底层是一个pipeline,先筛选,再聚合,如果反过来的话,就查不到相关的数据了。】
AggregationResults类实现了Iterable接口,所以我们可以迭代它,并打印结果。
3.2 获取平均城市人口最少的州
对于这个问题,我们需要四个环节:
- $group求出每个城市的人口总和
- $group计算每个州的平均城市人口数
- $sort环节按州的平均城市人口数,升序排列每个州
- $limit取第一个州,即为平均城市人口数最少的州
尽管不是必须的,我们还是使用一个额外的$project环节把结果格式化为StatePopulation数据模型。
GroupOperation sumTotalCityPop = group("state", "city")
.sum("pop").as("cityPop");
GroupOperation averageStatePop = group("_id.state")
.avg("cityPop").as("avgCityPop");
SortOperation sortByAvgPopAsc = sort(new Sort(Direction.ASC, "avgCityPop"));
LimitOperation limitToOnlyFirstDoc = limit(1);
ProjectionOperation projectToMatchModel = project()
.andExpression("_id").as("state")
.andExpression("avgCityPop").as("statePop");
Aggregation aggregation = newAggregation(
sumTotalCityPop, averageStatePop, sortByAvgPopAsc,
limitToOnlyFirstDoc, projectToMatchModel);
AggregationResults<StatePopulation> result = mongoTemplate
.aggregate(aggregation, "zips", StatePopulation.class);
StatePopulation smallestState = result.getUniqueMappedResult();
在这个例子中,我们已经知道结果中只会有一个document,因为我们已经在最后一个环节限制了输出document的个数。因此,我们可以调用getUniqueMappedResult()方法获得StatePopulation的实例。
另一个需要注意的地方是,我们在投影的环节,显式的把_id转换为州,而不是使用@id注解。
3.3 获取邮政编码个数最多和最少的州
这个例子中,我们需要三个环节:
- $group计算每个州的邮政编码个数
- $sort按邮政编码个数对州进行排序
- $group使用了操作符$first和$last查找邮政编码最多和最少的州
GroupOperation sumZips = group("state").count().as("zipCount");
SortOperation sortByCount = sort(Direction.ASC, "zipCount");
GroupOperation groupFirstAndLast = group().first("_id").as("minZipState")
.first("zipCount").as("minZipCount").last("_id").as("maxZipState")
.last("zipCount").as("maxZipCount");
Aggregation aggregation = newAggregation(sumZips, sortByCount, groupFirstAndLast);
AggregationResults<DBObject> result = mongoTemplate
.aggregate(aggregation, "zips", DBObject.class);
DBObject dbObject = result.getUniqueMappedResult();
这次我们没有使用任何模型,而是使用MongoDB驱动中已经提供的DBObject。
4. 结论
在这篇文章中,我们学习了如何使用Spring Data MongoDB中的投影方式获取数据库中document的特定字段。
我们也学习了Spring Data如何支持MongoDB的聚合框架。我们涉及到了主要的聚合方式——分组、投影、排序、数量和匹配,以及这些方式的具体的例子。完整的代码在github上。