【翻译】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上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341