SpringBoot中MongoDB聚合管道查询操作$facet$lookup$unwind$group

打印 上一主题 下一主题

主题 590|帖子 590|积分 1770

前言、官方文档、MongoTemplate中的概念

前言

最近在做基于SpringBoot的MongoDB的聚合管道操作,JSON语句不难写,但是理清楚逻辑、顺序很麻烦,而且在Java(Springboot)上操作聚合管道,部分操作符的使用不清楚,加之网上可以参考的示例很零散,很多不够直观全面。
所以在翻阅了官方文档和一些个人分享的技术文章后,自己做了测试验证,汇总了这篇笔记,分享一下基于SpringBoot的MongoDB的聚合管道操作。
主要是聚焦于理解MongoDB Template提供的两种实现聚合管道的操作,重点基于$group,$lookup,  $unwind,  $facet, 这几个操作符,实际代码中也有涉及到$match, $count, $sortByCount的使用。 
同时梳理一下MongoDB template中的几个定义(见“MongoTemplate中的概念”),希望有助于大家。
请大家配合标题导航食用,风味更佳~ 
 
禁止转载!!!!!!
 
官方文档

https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/
 
MongoTemplate中的概念


  • MongoTemplate:官方提供的操作MongoDB的对象。位于: org.springframework.data.mongodb.core。 使用的时候,需要注入。
  • Query:用于创建查询条件的对象。 位于:package org.springframework.data.mongodb.core.query。 使用时一般需要传入如"Criteria"构建的查询条件。
  • Criteria: 构建具体查询条件的对象,和Query位于同个包下。
  

  • AggregationOperation:聚合管道的操作对象,这是适用于Aggregate Pipeline Stages的操作,比如$group/$lookup/$unwind/$sort.......使用的时候,需要先构建对应的聚合操作,比如$group(需要构建具体操作), 可以创建多个,最后一并传入到Aggregation对象中,再交给template去执行管道聚合。  位于:
  • Aggregation:Pipeline stage的集合,也就是上面AggregationOperation的集合,把上面的所有聚合操作存在一起,template调用aggregate方法的时候,传入该对象。 
  • 以上类位于 package org.springframework.data.mongodb.core.aggregation;
 

  • Aggregates: Pipeline stage操作对象。 和Aggregation有几乎一样的功能,但是会更加灵活,一般除了预先提供的操作符,还可以自己传入Bson操作对象去灵活实现。 整体的使用难度,比Aggregation可能高一些。 


  • Bson、BsonDocument、BsonField:  Bson我理解就是灵活的表达式,查询条件、聚合操作符之类的构建定义,都可以由它接收,并最后传给template的aggregate方法去执行聚合操作。BsonDocument则是Bson的具体实现,用于灵活构建表达式的对象。 关于这部分,具体可以往下看。BsonField也是构建灵活的聚合表达式的一个类,比如快速地定义{"count": { $sum: 1 } ,作为聚合操作的一部分传入到具体的聚合阶段中。 
  • 以上类位于  package com.mongodb.client.model; Bson/BsonDocument则是另外的包中。org.bson中。感兴趣自行去源码中查找。 
开发环境和参考文档

JDK1.8 + Maven
SpringBoot(Springboot-starter-parent): 2.7.5
Mongodb(spring-boot-starter-data-mongodb)  4.6.1 
 参考文档:
http://www.mydlq.club/article/85/#1maven-%E5%BC%95%E5%85%A5%E7%9B%B8%E5%85%B3%E4%BE%9D%E8%B5%96 
https://learnku.com/articles/61052
 参考了以上网友的分享案例。
代码和案例

$count 和$match操作符

官方定义

$count: Passes a document to the next stage that contains a count of the number of documents input to the stage.  
https://www.mongodb.com/docs/manual/reference/operator/aggregation/count/
就是统计当前stage(聚合管道操作的阶段)存在的文档数量。
$match: Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage.
过滤符合条件的数据到下个pipeline stage。 
语法
  1. { $count: <string> // 这里的名称随便写,最后显示出来的结果就是 xxx : 总数 } <br><br>// match语法
复制代码
  { $match: {  } }  // 就是传入查询语句Json格式
 
在MongoDB中操作的官方示例
  1. // 数据
复制代码
{ "_id" : 1, "subject" : "History", "score" : 88 }
{ "_id" : 2, "subject" : "History", "score" : 92 }
{ "_id" : 3, "subject" : "History", "score" : 97 }
{ "_id" : 4, "subject" : "History", "score" : 71 }
{ "_id" : 5, "subject" : "History", "score" : 79 }
{ "_id" : 6, "subject" : "History", "score" : 83 }
// 执行
db.scores.aggregate(
// 先用match查找匹配的文档,然后直接用count统计当前match阶段存在的文档数量。
  [{$match: {score: {$gt: 80}}},
    {$count: "passing_scores"  // 这里的passing_scores 也可以是其他任意名称
    }]
)
// 返回结果
{ "passing_scores" : 4 }
 
MongoTemplate中实现的Java代码

数据参考上面官方示例。 以下分别是Aggregation和Aggregates的实现。 任意一种都可以。 
 
  1.     /**
  2.      * @Author zaoyu
  3.      */
  4.     @Autowired
  5.     private MongoTemplate mongoTemplate;
  6.     private String DEMO_COLLECTION = "demo";
  7.     /**
  8.      * 用Aggregates和Bson构建聚合操作对象,用预先生成的MongoCollection对象调用aggregate执行即可。
  9.      */
  10.     @Test
  11.     public void testCountWithAggregates(){
  12.         MongoCollection<Document> collection = mongoTemplate.getCollection(DEMO_COLLECTION);
  13.         // Aggregates提供各种操作符,返回一个Bson对象。这里用match,然后用Filters来实现过滤条件的构建,也是返回一个Bson对象。
  14.         Bson matchBson = Aggregates.match(Filters.gt("score", 80));
  15.         // 直接用Aggregates的count方法,如果不传自定义的名称,默认用“count”接收。
  16.         Bson countBson = Aggregates.count("myCount");
  17.         // 构建一个List<Bson>, 并把每一个聚合操作Bson加进去,最后传入aggregate方法中执行。
  18.         List<Bson> bsonList = new ArrayList<>();
  19.         bsonList.add(matchBson);
  20.         bsonList.add(countBson);
  21.         AggregateIterable<Document> resultList = collection.aggregate(bsonList);
  22.         for (Document document : resultList) {
  23.             System.out.println("result is :" + document);
  24.         }
  25.     }
  26.     /**
  27.      * 用Aggregation集合接收聚合操作,用MongoTemplate对象直接调用aggregate,传入聚合操作集合、表名、映射对象。
  28.      */
  29.     @Test
  30.     public void testCountWithAggregation(){
  31.         // 构建查询match条件:分数大于80
  32.         MatchOperation matchOperation = Aggregation.match(Criteria.where("score").gt(80));
  33.         // 构建count操作,用“myCount”名称接收
  34.         CountOperation countOperation = Aggregation.count().as("myCount");
  35.         // 传入多个aggregation(聚合操作),用Aggregation对象接收。
  36.         Aggregation aggregation = Aggregation.newAggregation(matchOperation, countOperation);
  37.         // 直接用mongoTemplate调用aggregate方法,传入aggregation集合,表名,还有用什么对象接收数据,这里我用Document接收,不再建类。
  38.         AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregation, DEMO_COLLECTION, Document.class);
  39.         for (Document document : resultList) {
  40.             System.out.println("result is :" + document);
  41.         }
  42.     }<br><br>// 以上2个方法的输出结果一样,如下。<br><br>result is :Document{{myCount=4}}<br>
复制代码
 
$group 操作符

官方定义

The $group stage separates documents into groups according to a "group key". The output is one document for each unique group key. A group key is often a field, or group of fields. The group key can also be the result of an expression. Use the _id field in the $group pipeline stage to set the group key.  
大概意思就是把文档做分组。 输出的格式是一条数据有一个唯一的分组键。 这里可以简单类比mysql 的group by分组。
 
语法
  1. {
  2.   $group:
  3.     {
  4.       _id: <expression>, // 用来分组的字段
  5.       <field1>: { <accumulator1> : <expression1> }, // 对某字段做处理 accumulator操作。
  6.       ...
  7.     }
  8. }
复制代码
在MongoDB中操作的官方示例
  1. // 插入数据
  2. db.sales.insertMany([
  3.   { "_id" : 1, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("2"), "date" : ISODate("2014-03-01T08:00:00Z") },
  4.   { "_id" : 2, "item" : "jkl", "price" : NumberDecimal("20"), "quantity" : NumberInt("1"), "date" : ISODate("2014-03-01T09:00:00Z") },
  5.   { "_id" : 3, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" : NumberInt( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },
  6.   { "_id" : 4, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" :  NumberInt("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },
  7.   { "_id" : 5, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },
  8.   { "_id" : 6, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },
  9.   { "_id" : 7, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("10") , "date" : ISODate("2015-09-10T08:43:00Z") },
  10.   { "_id" : 8, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") },
  11. ])
复制代码
// 执行group,这里还加上了match 和 project和sort。 一并使用展示。 
  1. db.getCollection("sales").aggregate(
  2.   // 第一个聚合管道:过滤出日期在2014-01-01到2015-01-01之间的数据
  3. [ {
  4.     $match : { "date": { $gte: new ISODate("2014-01-01"), $lt: new ISODate("2015-01-01") } }
  5.   },
  6. // 第二个聚合管道:处理一下日期格式,方便等下做group。  
  7. {$project:
  8.      {quantity:1,
  9.          price:1,
  10.          myDate:{"$dateToString":{format: "%Y-%m-%d", date: "$date"}}
  11. }},
  12. // 第三个聚合管道:分组统计,先按照日期分组,再统计每天的销售数量。        
  13. {$group:{_id:"$myDate",
  14.     perDayQuantity:{$sum:"$quantity"},
  15.     myCount: {$sum:1}
  16.    }},
  17. // 第四个聚合管道:按照每日销售数量降序排序
  18. {$sort:{"perDayQuantity":-1}}
  19. ])
复制代码
 
MongoTemplate中实现的Java代码
  1.     /**<br>   * @Author zaoyu
  2.      * Aggregation 实现match, group, sort。
  3.      */
  4.     @Test
  5.     public void testGroupAggregations(){
  6.         // 第一阶段,过滤查询日期介于14-1-1~15-1-1之间的数据,用Aggregation实现类MatchOperation接收。
  7.         MatchOperation match = Aggregation.match(Criteria
  8.                 .where("date").gte(Instant.parse("2014-01-01T08:00:00.000Z"))
  9.                 .andOperator(Criteria.where("date").lte(Instant.parse("2015-01-01T08:00:00.000Z"))));
  10.         // 第二阶段,处理一下日期格式,方便等下做group,用ProjectionOperation接收,也是Aggregation的实现类。
  11.         ProjectionOperation project = Aggregation.project("quantity", "price")
  12.                 .andExpression("{"$dateToString":{format: "%Y-%m-%d", date: "$date"}}").as("myDate");
  13.         // 第三阶段,分组统计,先按照日期分组,再统计每天的销售数量。
  14.         GroupOperation group = Aggregation.group("myDate")
  15.                 .sum("quantity").as("perDayQuantity")
  16.                 // 这里是计算文档条数
  17.                 .count().as("myCount");
  18.         // 第四阶段,排序。按照perDayQuantity字段升序展示。
  19.         SortOperation sort = Aggregation.sort(Sort.Direction.ASC, "perDayQuantity");
  20.         // 用newAggregation接收以上多个阶段的管道聚合指令,执行,得到结果。
  21.         Aggregation aggregations =Aggregation.newAggregation(match, project, group, sort);
  22.         AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, SALES_COLLECTION, Document.class);
  23.         for (Document document : resultList) {
  24.             System.out.println("result is :" + document);
  25.         }
  26.     }<br>// 返回结果<br>
复制代码
result is :Document{{_id=2014-03-01, perDayQuantity=3, myCount=2}}
result is :Document{{_id=2014-03-15, perDayQuantity=10, myCount=1}}
result is :Document{{_id=2014-04-04, perDayQuantity=30, myCount=2}}
 
$unwind 操作符

官方定义

Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
大概意思就是把输入文档的数组字段按元素一个个拆分出来,并和原来的数据一并形成一条新文档输出。 好比原来10条数据,其中每条数据都有长度为3的数组,那么拆出来(在元素不重复的情况下),会得到30条数据。
 
语法
  1. { $unwind: <field path> }  
  2. {
  3.   $unwind:
  4.     {
  5.       path: <field path>,  // path是固定名称,沿用即可。 <field path> 是数组字段,就是你要拆分的字段(值得是一个数组,不然没有意义)
  6.       includeArrayIndex: <string>,
  7.       preserveNullAndEmptyArrays: <boolean>
  8.     }
  9. }
复制代码
在MongoDB中操作的官方示例
  1. // 插入数据
  2. db.inventory.insertOne({ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] })
  3. // 执行unwind操作,这里是把 sizes字段的数组拆分出来
  4. db.inventory.aggregate( [ { $unwind : "$sizes" } ] )
  5. // 执行结果  可以看到每条数据的sizes不再是list,而是具体的元素。
  6. { "_id" : 1, "item" : "ABC1", "sizes" : "S" }
  7. { "_id" : 1, "item" : "ABC1", "sizes" : "M" }
  8. { "_id" : 1, "item" : "ABC1", "sizes" : "L" }
复制代码
注意,如果要拆分的字段是一个空数组或者null,那么实际输出的数据,不会包含那条数据。 如下示例。
  1. // 插入多条数据,这里还放了三条特殊数据,一个是空集合,一个是null,一个是没有要拆分的字段 sizes
  2. db.clothing.insertMany([
  3.   { "_id" : 1, "item" : "Shirt", "sizes": [ "S", "M", "L"] },
  4.   { "_id" : 2, "item" : "Shorts", "sizes" : [ ] },
  5.   { "_id" : 3, "item" : "Hat", "sizes": "M" },
  6.   { "_id" : 4, "item" : "Gloves" },
  7.   { "_id" : 5, "item" : "Scarf", "sizes" : null }
  8. ])
  9. // 执行$unwind
  10. db.clothing.aggregate( [ { $unwind: { path: "$sizes" } } ] )
  11. // 返回结果, 可以看到,sizes值为空数组和null的id=2以及id=5的数据都没有展示出来,同时没有该字段的id=4,也没有展示出来。
  12. { _id: 1, item: 'Shirt', sizes: 'S' },
  13. { _id: 1, item: 'Shirt', sizes: 'M' },
  14. { _id: 1, item: 'Shirt', sizes: 'L' },
  15. { _id: 3, item: 'Hat', sizes: 'M' }
复制代码
MongoTemplate中实现的Java代码

 以下分别是Aggregation和Aggregates的实现
  1.     /**
  2.      * @Author zaoyu
  3.      * Aggregation 实现$unwind
  4.      */
  5.     @Test
  6.     public void testUnwindAggregations() {
  7.         String CLOTHING_COLLECTION = "clothing";
  8.         // 调用Aggregation中的unwind的聚合操作符
  9.         UnwindOperation unwind = Aggregation.unwind("sizes");
  10.         // 用newAggregation接收管道聚合指令,执行,得到结果。
  11.         Aggregation aggregations =Aggregation.newAggregation(unwind);
  12.         // mongoTemplate 直接调用aggregate方法,传入Aggregation对象,基于的表,映射类(这里简单化,我用Document)
  13.         AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, CLOTHING_COLLECTION, Document.class);
  14.         for (Document document : resultList) {
  15.             System.out.println("result is :" + document);
  16.         }
  17.     }
  18.     /**
  19.      * @Author zaoyu
  20.      * Aggregates/Bson 实现$unwind
  21.      */
  22.     @Test
  23.     public void testUnwindAggregates(){
  24.         String CLOTHING_COLLECTION = "clothing";
  25.         // 调用Aggregates的unwind聚合操作符 注意,Aggregates这里需要传入$
  26.         Bson unwindBson = Aggregates.unwind("$sizes");
  27.         // 建一个List<Bson> 把unwindBson传进去
  28.         List<Bson> bsonList = new ArrayList<>();
  29.         bsonList.add(unwindBson);
  30.         // mongoTemplate先获得对应的collection对象,然后调用aggregate,传入List<Bson> 获得结果
  31.         MongoCollection<Document> collection = mongoTemplate.getCollection(CLOTHING_COLLECTION);
  32.         AggregateIterable<Document> resultList = collection.aggregate(bsonList);
  33.         for (Document document : resultList) {
  34.             System.out.println("result is :" + document);
  35.         }
  36.     }
复制代码
 
$lookup 操作符

官方定义

Performs a left outer join to a collection in the same database to filter in documents from the "joined" collection for processing. The  $lookup  stage adds a new array field to each input document. The new array field contains the matching documents from the "joined" collection. 

The $lookup  stage passes these reshaped documents to the next stage.

Starting in MongoDB 5.1,  $lookup   works across sharded collections. 
其实可以简单理解类比Mysql的子查询。 会把另外一张表匹配的数据,作为一个数组存入到当前数据中,需要自定义一个字段来接收显示。  
类比如下的sql语句
  1. SELECT *, <output array field>
  2. FROM collection
  3. WHERE <output array field> IN (
  4.    SELECT *
  5.    FROM <collection to join>
  6.    WHERE <foreignField> = <collection.localField>
  7. );
复制代码
【特别注意】如果当前DB是集群部署,那么在DB版本为5.1之前的情况,$lookup是不会生效的。  如果你数据库是集群的,然后又要用$lookup,一定要检查版本是否大于等于5.1,否则是查不出来的。  前阵子不知道这个,一直没有头绪为什么数据查不出来。 
语法
  1. {
  2.    $lookup:
  3.      {
  4.        from: <collection to join>,  // 要联表查的表名
  5.        localField: <field from the input documents>, // 当前表的要和联表关联的字段
  6.        foreignField: <field from the documents of the "from" collection>, // 要被关联表的外键字段
  7.        as: <output array field> // 定义一个字段接收匹配关联的数据
  8.      }
  9. }
复制代码
 
在MongoDB中操作的官方示例
  1. // 插入表orders数据  
  2. db.orders.insertMany( [
  3.    { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },
  4.    { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },
  5.    { "_id" : 3  }
  6. ] )
  7. // 插入表inventory数据
  8. db.inventory.insertMany( [
  9.    { "_id" : 1, "sku" : "almonds", "description": "product 1", "instock" : 120 },
  10.    { "_id" : 2, "sku" : "bread", "description": "product 2", "instock" : 80 },
  11.    { "_id" : 3, "sku" : "cashews", "description": "product 3", "instock" : 60 },
  12.    { "_id" : 4, "sku" : "pecans", "description": "product 4", "instock" : 70 },
  13.    { "_id" : 5, "sku": null, "description": "Incomplete" },
  14.    { "_id" : 6 }
  15. ] )
复制代码
执行代码
  1. db.orders.aggregate( [ // db.orders 表示基于orders做聚合操作
  2.    {
  3.      $lookup:
  4.        {
  5.          from: "inventory",   // 联表inventory
  6.          localField: "item",  // 当前orders的字段
  7.          foreignField: "sku", // inventory中的sku字段,和orders的item关联
  8.          as: "inventory_docs" // 定义一个字段名接收 inventory中sku 和orders的item相同的数据,数组形式。
  9.        }
  10.   }
  11. ] )
复制代码
返回结果
  1. {
  2.    "_id" : 1,
  3.    "item" : "almonds",
  4.    "price" : 12,
  5.    "quantity" : 2,
  6.    "inventory_docs" : [  // 这个inventory_docs字段就是自己命名的字段,存储着来自inventory的数据
  7.       { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 }
  8.    ]
  9. }
  10. {
  11.    "_id" : 2,
  12.    "item" : "pecans",
  13.    "price" : 20,
  14.    "quantity" : 1,
  15.    "inventory_docs" : [
  16.       { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 }
  17.    ]
  18. }
  19. {
  20.    "_id" : 3,
  21.    "inventory_docs" : [
  22.       { "_id" : 5, "sku" : null, "description" : "Incomplete" },
  23.       { "_id" : 6 }
  24.    ]
  25. }
复制代码
 
MongoTemplate中实现的Java代码

以下分别是Aggregation和Aggregates的实现
  1.     /**
  2.      * @Author zaoyu
  3.      * Aggregation 实现$lookup
  4.      */
  5.     @Test
  6.     public void testLookupAggregations(){
  7.         String INVENTORY_COLLECTION = "inventory";
  8.         String ORDERS_COLLECTION = "orders";
  9.         // Aggregation类,直接可以调用lookup方法,传入要关联的表、当前表和关联表关联的字段、要关联的表的字段、自定义名称接收关联匹配的数据
  10.         LookupOperation lookup = Aggregation.lookup(INVENTORY_COLLECTION, "item", "sku", "inventory_docs");
  11.         // 用newAggregation接收管道聚合指令,执行,得到结果。
  12.         Aggregation aggregations =Aggregation.newAggregation(lookup);
  13.         // mongoTemplate 直接调用aggregate方法,传入Aggregation对象,基于的表,映射类(这里简单化,我用Document)
  14.         AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, ORDERS_COLLECTION, Document.class);
  15.         for (Document document : resultList) {
  16.             System.out.println("result is :" + document);
  17.         }
  18.     }
  19.     /**
  20.      * @Author zaoyu
  21.      * Aggregates/Bson 实现$lookup
  22.      */
  23.     @Test
  24.     public void testLookupAggregates(){
  25.         String INVENTORY_COLLECTION = "inventory";
  26.         String ORDERS_COLLECTION = "orders";
  27.         // 这里用Aggregates类直接调用lookup,传入的参数和上面的Aggregations的lookup是一样的,只不过这里返回的结果是一个Bson对象。
  28.         Bson lookupBson = Aggregates.lookup(INVENTORY_COLLECTION, "item", "sku", "inventory_docs");
  29.         // 建一个List<Bson> 把lookupBson传进去
  30.         List<Bson> bsonList = new ArrayList<>();
  31.         bsonList.add(lookupBson);
  32.         // mongoTemplate先获得对应的collection对象,然后调用aggregate,传入List<Bson> 获得结果
  33.         MongoCollection<Document> collection = mongoTemplate.getCollection(ORDERS_COLLECTION);
  34.         AggregateIterable<Document> resultList = collection.aggregate(bsonList);
  35.         for (Document document : resultList) {
  36.             System.out.println("result is :" + document);
  37.         }
  38.     }
复制代码
 
返回结果
  1. result is :Document{{_id=1.0, item=almonds, price=12.0, quantity=2.0, inventory_docs=[Document{{_id=1.0, sku=almonds, description=product 1, instock=120.0}}]}}
  2. result is :Document{{_id=2.0, item=pecans, price=20.0, quantity=1.0, inventory_docs=[Document{{_id=4.0, sku=pecans, description=product 4, instock=70.0}}]}}
  3. result is :Document{{_id=3.0, inventory_docs=[Document{{_id=5.0, sku=null, description=Incomplete}}, Document{{_id=6.0}}]}}
复制代码
 
$facet 操作符

官方定义

 Processes multiple aggregation pipelines within a single stage on the same set of input documents. Each sub-pipeline has its own field in the output document where its results are stored as an array of documents.

Input documents are passed to the $facet stage only once. $facet enables various aggregations on the same set of input documents, without needing to retrieve the input documents multiple times.
简单来说,就是facet可以实现在facet管道操作完成多个stage管道操作。减少获取输入文档的次数。
 
我个人觉得有种场景很适合使用facet:分页查询文档数据的同时,把符合查询条件的总数也查询出来的场景下,如果使用$facet,同时获取分页数据和总数,不用做两次数据库查询(分别查询分页数据和总数)。
 
语法
  1. { $facet:
  2.    {
  3.       <outputField1>: [ <stage1>, <stage2>, ... ],  // 这里outputpufield 是自己定义的用来接收stage集合返回的文档数据。  
  4.       <outputField2>: [ <stage1>, <stage2>, ... ],  // 可以基于上一个Facet继续做facet
  5.       ...
  6.    }
  7. }
复制代码
 
在MongoDB中操作的官方示例
  1. // 数据  插入artwork 表中
  2. { "_id" : 1, "title" : "The Pillars of Society", "artist" : "Grosz", "year" : 1926,
  3.   "price" : NumberDecimal("199.99"),
  4.   "tags" : [ "painting", "satire", "Expressionism", "caricature" ] }
  5. { "_id" : 2, "title" : "Melancholy III", "artist" : "Munch", "year" : 1902,
  6.   "price" : NumberDecimal("280.00"),
  7.   "tags" : [ "woodcut", "Expressionism" ] }
  8. { "_id" : 3, "title" : "Dancer", "artist" : "Miro", "year" : 1925,
  9.   "price" : NumberDecimal("76.04"),
  10.   "tags" : [ "oil", "Surrealism", "painting" ] }
  11. { "_id" : 4, "title" : "The Great Wave off Kanagawa", "artist" : "Hokusai",
  12.   "price" : NumberDecimal("167.30"),
  13.   "tags" : [ "woodblock", "ukiyo-e" ] }
  14. { "_id" : 5, "title" : "The Persistence of Memory", "artist" : "Dali", "year" : 1931,
  15.   "price" : NumberDecimal("483.00"),
  16.   "tags" : [ "Surrealism", "painting", "oil" ] }
  17. { "_id" : 6, "title" : "Composition VII", "artist" : "Kandinsky", "year" : 1913,
  18.   "price" : NumberDecimal("385.00"),
  19.   "tags" : [ "oil", "painting", "abstract" ] }
  20. { "_id" : 7, "title" : "The Scream", "artist" : "Munch", "year" : 1893,
  21.   "tags" : [ "Expressionism", "painting", "oil" ] }
  22. { "_id" : 8, "title" : "Blue Flower", "artist" : "O'Keefe", "year" : 1918,
  23.   "price" : NumberDecimal("118.42"),
  24.   "tags" : [ "abstract", "painting" ] }
  25. // 执行$facet聚合
  26. db.artwork.aggregate( [
  27.   {
  28.     $facet: {
  29.         // 第一个Facet操作,按照tag分类:先用unwind拆分tags字段的数组值,交给下一个聚合 $sortByCount, 按照tags的个数排序。
  30.       "categorizedByTags": [
  31.         { $unwind: "$tags" },
  32.         { $sortByCount: "$tags" }
  33.       ],
  34.     // 第二个Facet操作,按照price分类:先过滤数据(只处理存在price数据的文档),然后执行$bucket按照价格区间分组 0~150,151~200, 201~300, 301~400这样。
  35.       "categorizedByPrice": [
  36.         { $match: { price: { $exists: 1 } } },
  37.         {
  38.           $bucket: {
  39.             groupBy: "$price",
  40.             boundaries: [  0, 150, 200, 300, 400 ],
  41.             default: "Other",
  42.             output: {
  43.               "count": { $sum: 1 },
  44.               "titles": { $push: "$title" }
  45.             }
  46.           }
  47.         }
  48.       ],
  49. // 第三个Facet, 按照years分类。 分成4个区间。
  50.       "categorizedByYears(Auto)": [
  51.         {
  52.           $bucketAuto: {
  53.             groupBy: "$year",
  54.             buckets: 4
  55.           }
  56.         }
  57.       ]
  58.     }
  59.   }
  60. ])
  61.    
复制代码
 
MongoTemplate中实现的Java代码

注:以下代码的实现,数据来源参考上边的官方示例的数据, artwork表。 请自行插入数据。
 1. 使用Aggregation对象实现
  1.     /**
  2.      * @Author zaoyu
  3.      * Aggregation 实现$facet
  4.      */
  5.     @Test
  6.     public void testFacetAggregations(){
  7.         String ARTWORK_COLLECTION = "artwork";
  8.         // Facet中第一组分类(categorizedByTags)的两个聚合操作unwind 和 sortByCount
  9.         UnwindOperation unwindForByTags = Aggregation.unwind("$tags");
  10.         SortByCountOperation sortByCountForByTags = Aggregation.sortByCount("$tags");
  11.         // Facet中第二组分类(categorizedByPrice)的聚合操作match 和 match
  12.         MatchOperation matchForByPrice = Aggregation.match(Criteria.where("price").exists(true));
  13.         // 分别传入bucket分组的字段price,设置区间值,并设置桶内条数统计和值(这里用titles接收title的值)
  14.         BucketOperation bucketForByPrice = Aggregation.bucket("$price")
  15.                 .withBoundaries(0, 150, 200, 300, 400)
  16.                 .withDefaultBucket("Other")
  17.                 .andOutput("count").sum(1).as("count")
  18.                 .andOutput("$title").push().as("titles");
  19.         // Facet中第三组分类 (categorizedByYears(Auto))的聚合操作,按年自动分成4个区间。
  20.         BucketAutoOperation bucketForByYears = Aggregation.bucketAuto("$year", 4);
  21.         // Aggregation调用facet方法,按照组别分类顺序,把每一组的聚合操作和输出的名称传进去。
  22.         FacetOperation facetOperation = Aggregation.facet(unwindForByTags, sortByCountForByTags).as("categorizedByTags")
  23.                 .and(matchForByPrice, bucketForByPrice).as("categorizedByPrice")
  24.                 .and(bucketForByYears).as("categorizedByYears(Auto)");
  25.         // 把facetOperation传入newAggregation得到Aggregation对象,调用mongoTemplate的Aggregate方法执行得到结果
  26.         Aggregation aggregation = Aggregation.newAggregation(facetOperation);
  27.         AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregation, ARTWORK_COLLECTION, Document.class);
  28.         for (Document document : resultList) {
  29.             System.out.println("result is :" + document);
  30.         }
  31.     }
复制代码
2. 使用Aggregates实现
  1.   /**
  2.      * @Author zaoyu
  3.      * Aggregates 实现$facet
  4.      */
  5.     @Test
  6.     public void testFacetAggregates() {
  7.         String ARTWORK_COLLECTION = "artwork";
  8.         // Facet中第一组分类(categorizedByTags)的两个聚合操作unwind 和 sortByCount
  9.         Bson unwindBsonForByTags = Aggregates.unwind("$tags");
  10.         Bson sortByCountBsonForByTags = Aggregates.sortByCount("$tags");
  11.         // 新建Facet对象,传入第一组分类的接收名称,以及在第一组分类中要做的聚合操作。
  12.         Facet categorizedByTags = new Facet("categorizedByTags", unwindBsonForByTags, sortByCountBsonForByTags);
  13.         // Facet中第二组分类(categorizedByPrice)的聚合操作match 和 match
  14.         Bson matchBsonForPrice = Aggregates.match(Filters.exists("price"));
  15.         // 这里面要新建BsonField构建 {"count": { $sum: 1 }  和 "titles": { $push: "$title" }} 作为第二组分类中$Bucket聚合操作中output值
  16.         BsonField countOutput = new BsonField("count", new Document("$sum", 1));
  17.         BsonField titleOutput = new BsonField("titles", new Document("$push", "$price"));
  18.         // 上面2个操作传入到BucketOption对象,最后传到bucket操作
  19.         BucketOptions bucketOptions = new BucketOptions().defaultBucket("Other").output(countOutput).output(titleOutput);
  20.         Bson bucketBsonForByPrice = Aggregates.bucket("$price", Arrays.asList(0, 150, 200, 300, 400), bucketOptions);
  21.         Facet categorizedByPrice = new Facet("categorizedByPrice", matchBsonForPrice, bucketBsonForByPrice);
  22.         // Facet中第三组分类 (categorizedByYears(Auto))的聚合操作,按年自动分成4个区间。
  23.         Bson bucketAutoBsonForByYears = Aggregates.bucketAuto("$year", 4);
  24.         Facet categorizedByYears = new Facet("categorizedByYears", bucketAutoBsonForByYears);
  25.         // 新建一个List<Facet>把每组分类的Facet对象传进去。
  26.         List<Facet> facetList = new ArrayList<>();
  27.         facetList.add(categorizedByTags);
  28.         facetList.add(categorizedByPrice);
  29.         facetList.add(categorizedByYears);
  30.         // 调用Aggregates的facet方法,传入List<Facet>得到最终Bson对象,并添加到Bson集合中。
  31.         Bson facetBson = Aggregates.facet(facetList);
  32.         List<Bson> bsonList = new ArrayList<>();
  33.         bsonList.add(facetBson);
  34.         // 调用方法执行得到结果
  35.         MongoCollection<Document> collection = mongoTemplate.getCollection(ARTWORK_COLLECTION);
  36.         AggregateIterable<Document> resultList = collection.aggregate(bsonList);
  37.         for (Document document : resultList) {
  38.             System.out.println("result is :" + document);
  39.         }
  40.     }
复制代码
最终返回结果, 二者一样。
  1. result is :Document{{categorizedByTags=[Document{{_id=painting, count=6}}, Document{{_id=oil, count=4}}, Document{{_id=Expressionism, count=3}}, Document{{_id=Surrealism, count=2}}, Document{{_id=abstract, count=2}}, Document{{_id=woodblock, count=1}}, Document{{_id=ukiyo-e, count=1}}, Document{{_id=satire, count=1}}, Document{{_id=caricature, count=1}}, Document{{_id=woodcut, count=1}}], categorizedByPrice=[Document{{_id=0, titles=[76.04, 118.42]}}, Document{{_id=150, titles=[199.99, 167.30]}}, Document{{_id=200, titles=[280.00]}}, Document{{_id=300, titles=[385.00]}}, Document{{_id=Other, titles=[483.00]}}], categorizedByYears=[Document{{_id=Document{{min=null, max=1902.0}}, count=2}}, Document{{_id=Document{{min=1902.0, max=1918.0}}, count=2}}, Document{{_id=Document{{min=1918.0, max=1926.0}}, count=2}}, Document{{_id=Document{{min=1926.0, max=1931.0}}, count=2}}]}}
复制代码
 
 小结

整体来说,MonogoDB官方提供了很详细的资料,但是对于Java 层面的操作,或者说SpringBoot层面的操作,文档就比较简单。
个人感觉而言,Aggregations提供的方法比较直接,更适合不太熟悉Springboot上操作Mongo的同学来使用,而Aggregates会更加灵活,但是需要你知道Document, BsonField, Bson之间的转换和获取。 
 
希望这篇文章能帮到大家,有错漏之处,欢迎指正。 
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南飓风

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表