【翻译】Spring Data Mongo: 投影和聚合

原文地址: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中,可以通过MongoTemplateMongoRepository来使用投影。

在继续深入之前,让我们先来看下即将用到的数据模型:

@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。在这个例子中,字段是一个原生类型或者他们的包装类,所以这些被排除的字段的值就是原生类型的缺省值。

例如,Stringnullint/Integer是0,而boolean/Booleanfalse

因此在上面的例子中,name字段是Johnidnull,而age0

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);

请注意,MatchOperationProjectionOperation都实现了* AggregationOperation*。其他聚合管道还有一些类似的实现。 OutType是期望的结果的数据模型。

现在,我们要看几个具体的例子。这些例子涵盖了主要的聚合管道和操作符。

这篇文章中使用的数据集,列出了美国所有的邮政编码,可以从这里下载到全部数据。

test数据库中引入这个数据集,collection的名字为zips,让我们看看其中一个document。

{
    "_id" : "01001",
    "city" : "AGAWAM",
    "loc" : [
        -72.622739,
        42.070206
    ],
    "pop" : 15338,
    "state" : "MA"
}

为了简化和代码整洁,下面的代码片段中,我们都假定Aggregation类的所有静态方法都已经静态的引入了。

3.1 获取所有人口大于1000万的州,并按人口数降序排列

这个例子中,我们有3个管道

  1. $group环节按邮政编码对人口求和
  2. $match环节找出那些人口超过1000万的州
  3. $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 获取平均城市人口最少的州

对于这个问题,我们需要四个环节:

  1. $group求出每个城市的人口总和
  2. $group计算每个州的平均城市人口数
  3. $sort环节按州的平均城市人口数,升序排列每个州
  4. $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 获取邮政编码个数最多和最少的州

这个例子中,我们需要三个环节:

  1. $group计算每个州的邮政编码个数
  2. $sort按邮政编码个数对州进行排序
  3. $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上。

点赞