主頁 > 資料庫 > SpringBoot中MongoDB聚合管道查詢操作$facet$lookup$unwind$group

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

2022-12-12 07:37:24 資料庫

前言、官方檔案、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, 

語法

{ $count: <string> // 這里的名稱隨便寫,最后顯示出來的結果就是 xxx : 總數 } 

// match語法

  { $match: { <query> } }  // 就是傳入查詢陳述句Json格式

 

在MongoDB中操作的官方示例

// 資料

{ "_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的實作, 任意一種都可以, 

 

    /**
     * @Author zaoyu
     */
    @Autowired
    private MongoTemplate mongoTemplate;

    private String DEMO_COLLECTION = "demo";


    /**
     * 用Aggregates和Bson構建聚合操作物件,用預先生成的MongoCollection物件呼叫aggregate執行即可,
     */
    @Test
    public void testCountWithAggregates(){
        MongoCollection<Document> collection = mongoTemplate.getCollection(DEMO_COLLECTION);
        // Aggregates提供各種運算子,回傳一個Bson物件,這里用match,然后用Filters來實作過濾條件的構建,也是回傳一個Bson物件,
        Bson matchBson = Aggregates.match(Filters.gt("score", 80));
        // 直接用Aggregates的count方法,如果不傳自定義的名稱,默認用“count”接收,
        Bson countBson = Aggregates.count("myCount");
        // 構建一個List<Bson>, 并把每一個聚合操作Bson加進去,最后傳入aggregate方法中執行,
        List<Bson> bsonList = new ArrayList<>();
        bsonList.add(matchBson);
        bsonList.add(countBson);
        AggregateIterable<Document> resultList = collection.aggregate(bsonList);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

    /**
     * 用Aggregation集合接收聚合操作,用MongoTemplate物件直接呼叫aggregate,傳入聚合操作集合、表名、映射物件,
     */
    @Test
    public void testCountWithAggregation(){
        // 構建查詢match條件:分數大于80
        MatchOperation matchOperation = Aggregation.match(Criteria.where("score").gt(80));
        // 構建count操作,用“myCount”名稱接收
        CountOperation countOperation = Aggregation.count().as("myCount");
        // 傳入多個aggregation(聚合操作),用Aggregation物件接收,
        Aggregation aggregation = Aggregation.newAggregation(matchOperation, countOperation);
        // 直接用mongoTemplate呼叫aggregate方法,傳入aggregation集合,表名,還有用什么物件接收資料,這里我用Document接收,不再建類,
        AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregation, DEMO_COLLECTION, Document.class);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

// 以上2個方法的輸出結果一樣,如下,

result is :Document{{myCount=4}}

 

$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分組,

 

語法

{
  $group:
    {
      _id: <expression>, // 用來分組的欄位
      <field1>: { <accumulator1> : <expression1> }, // 對某欄位做處理 accumulator操作, 
      ...
    }
 }

在MongoDB中操作的官方示例

// 插入資料
db.sales.insertMany([
  { "_id" : 1, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("2"), "date" : ISODate("2014-03-01T08:00:00Z") },
  { "_id" : 2, "item" : "jkl", "price" : NumberDecimal("20"), "quantity" : NumberInt("1"), "date" : ISODate("2014-03-01T09:00:00Z") },
  { "_id" : 3, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" : NumberInt( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },
  { "_id" : 4, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" :  NumberInt("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },
  { "_id" : 5, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },
  { "_id" : 6, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },
  { "_id" : 7, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("10") , "date" : ISODate("2015-09-10T08:43:00Z") },
  { "_id" : 8, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") },
])

// 執行group,這里還加上了match 和 project和sort, 一并使用展示, 

db.getCollection("sales").aggregate(
  // 第一個聚合管道:過濾出日期在2014-01-01到2015-01-01之間的資料
[ {
    $match : { "date": { $gte: new ISODate("2014-01-01"), $lt: new ISODate("2015-01-01") } }
  },
 // 第二個聚合管道:處理一下日期格式,方便等下做group,  
 {$project: 
     {quantity:1, 
         price:1,
         myDate:{"$dateToString":{format: "%Y-%m-%d", date: "$date"}}
 }},
 // 第三個聚合管道:分組統計,先按照日期分組,再統計每天的銷售數量,        
 {$group:{_id:"$myDate", 
    perDayQuantity:{$sum:"$quantity"},
    myCount: {$sum:1}
   }},
 // 第四個聚合管道:按照每日銷售數量降序排序
 {$sort:{"perDayQuantity":-1}}
])

 

MongoTemplate中實作的Java代碼

    /**
   * @Author zaoyu
* Aggregation 實作match, group, sort, */ @Test public void testGroupAggregations(){ // 第一階段,過濾查詢日期介于14-1-1~15-1-1之間的資料,用Aggregation實作類MatchOperation接收, MatchOperation match = Aggregation.match(Criteria .where("date").gte(Instant.parse("2014-01-01T08:00:00.000Z")) .andOperator(Criteria.where("date").lte(Instant.parse("2015-01-01T08:00:00.000Z")))); // 第二階段,處理一下日期格式,方便等下做group,用ProjectionOperation接收,也是Aggregation的實作類, ProjectionOperation project = Aggregation.project("quantity", "price") .andExpression("{\"$dateToString\":{format: \"%Y-%m-%d\", date: \"$date\"}}").as("myDate"); // 第三階段,分組統計,先按照日期分組,再統計每天的銷售數量, GroupOperation group = Aggregation.group("myDate") .sum("quantity").as("perDayQuantity") // 這里是計算檔案條數 .count().as("myCount"); // 第四階段,排序,按照perDayQuantity欄位升序展示, SortOperation sort = Aggregation.sort(Sort.Direction.ASC, "perDayQuantity"); // 用newAggregation接收以上多個階段的管道聚合指令,執行,得到結果, Aggregation aggregations =Aggregation.newAggregation(match, project, group, sort); AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, SALES_COLLECTION, Document.class); for (Document document : resultList) { System.out.println("result is :" + document); } }
// 回傳結果

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條資料,

 

語法

{ $unwind: <field path> }  

和

{
  $unwind:
    {
      path: <field path>,  // path是固定名稱,沿用即可, <field path> 是陣列欄位,就是你要拆分的欄位(值得是一個陣列,不然沒有意義)
      includeArrayIndex: <string>,
      preserveNullAndEmptyArrays: <boolean>
    }
}

在MongoDB中操作的官方示例

// 插入資料
db.inventory.insertOne({ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] })

// 執行unwind操作,這里是把 sizes欄位的陣列拆分出來 
db.inventory.aggregate( [ { $unwind : "$sizes" } ] )

// 執行結果  可以看到每條資料的sizes不再是list,而是具體的元素,
{ "_id" : 1, "item" : "ABC1", "sizes" : "S" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "M" }
{ "_id" : 1, "item" : "ABC1", "sizes" : "L" }

注意,如果要拆分的欄位是一個空陣列或者null,那么實際輸出的資料,不會包含那條資料, 如下示例,

// 插入多條資料,這里還放了三條特殊資料,一個是空集合,一個是null,一個是沒有要拆分的欄位 sizes
db.clothing.insertMany([
  { "_id" : 1, "item" : "Shirt", "sizes": [ "S", "M", "L"] },
  { "_id" : 2, "item" : "Shorts", "sizes" : [ ] },
  { "_id" : 3, "item" : "Hat", "sizes": "M" },
  { "_id" : 4, "item" : "Gloves" },
  { "_id" : 5, "item" : "Scarf", "sizes" : null }
])

// 執行$unwind
db.clothing.aggregate( [ { $unwind: { path: "$sizes" } } ] )

// 回傳結果, 可以看到,sizes值為空陣列和null的id=2以及id=5的資料都沒有展示出來,同時沒有該欄位的id=4,也沒有展示出來,
{ _id: 1, item: 'Shirt', sizes: 'S' },
{ _id: 1, item: 'Shirt', sizes: 'M' },
{ _id: 1, item: 'Shirt', sizes: 'L' },
{ _id: 3, item: 'Hat', sizes: 'M' }

MongoTemplate中實作的Java代碼

 以下分別是Aggregation和Aggregates的實作

    /**
     * @Author zaoyu
     * Aggregation 實作$unwind
     */
    @Test
    public void testUnwindAggregations() {
        String CLOTHING_COLLECTION = "clothing";
        // 呼叫Aggregation中的unwind的聚合運算子
        UnwindOperation unwind = Aggregation.unwind("sizes");
        // 用newAggregation接收管道聚合指令,執行,得到結果,
        Aggregation aggregations =Aggregation.newAggregation(unwind);
        // mongoTemplate 直接呼叫aggregate方法,傳入Aggregation物件,基于的表,映射類(這里簡單化,我用Document)
        AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, CLOTHING_COLLECTION, Document.class);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }
    /**
     * @Author zaoyu
     * Aggregates/Bson 實作$unwind
     */
    @Test
    public void testUnwindAggregates(){
        String CLOTHING_COLLECTION = "clothing";
        // 呼叫Aggregates的unwind聚合運算子 注意,Aggregates這里需要傳入$
        Bson unwindBson = Aggregates.unwind("$sizes");
        // 建一個List<Bson> 把unwindBson傳進去
        List<Bson> bsonList = new ArrayList<>();
        bsonList.add(unwindBson);
        // mongoTemplate先獲得對應的collection物件,然后呼叫aggregate,傳入List<Bson> 獲得結果
        MongoCollection<Document> collection = mongoTemplate.getCollection(CLOTHING_COLLECTION);
        AggregateIterable<Document> resultList = collection.aggregate(bsonList);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

 

$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陳述句

SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (
   SELECT *
   FROM <collection to join>
   WHERE <foreignField> = <collection.localField>
);

【特別注意】如果當前DB是集群部署,那么在DB版本為5.1之前的情況,$lookup是不會生效的,  如果你資料庫是集群的,然后又要用$lookup,一定要檢查版本是否大于等于5.1,否則是查不出來的,  前陣子不知道這個,一直沒有頭緒為什么資料查不出來, 

語法

{
   $lookup:
     {
       from: <collection to join>,  // 要聯表查的表名
       localField: <field from the input documents>, // 當前表的要和聯表關聯的欄位
       foreignField: <field from the documents of the "from" collection>, // 要被關聯表的外鍵欄位 
       as: <output array field> // 定義一個欄位接收匹配關聯的資料
     }
}

 

在MongoDB中操作的官方示例

// 插入表orders資料  
db.orders.insertMany( [
   { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 },
   { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 },
   { "_id" : 3  }
] )

// 插入表inventory資料
db.inventory.insertMany( [
   { "_id" : 1, "sku" : "almonds", "description": "product 1", "instock" : 120 },
   { "_id" : 2, "sku" : "bread", "description": "product 2", "instock" : 80 },
   { "_id" : 3, "sku" : "cashews", "description": "product 3", "instock" : 60 },
   { "_id" : 4, "sku" : "pecans", "description": "product 4", "instock" : 70 },
   { "_id" : 5, "sku": null, "description": "Incomplete" },
   { "_id" : 6 }
] )

執行代碼

db.orders.aggregate( [ // db.orders 表示基于orders做聚合操作
   {
     $lookup:
       {
         from: "inventory",   // 聯表inventory 
         localField: "item",  // 當前orders的欄位
         foreignField: "sku", // inventory中的sku欄位,和orders的item關聯 
         as: "inventory_docs" // 定義一個欄位名接收 inventory中sku 和orders的item相同的資料,陣列形式, 
       }
  }
] )

回傳結果

{
   "_id" : 1,
   "item" : "almonds",
   "price" : 12,
   "quantity" : 2,
   "inventory_docs" : [  // 這個inventory_docs欄位就是自己命名的欄位,存盤著來自inventory的資料
      { "_id" : 1, "sku" : "almonds", "description" : "product 1", "instock" : 120 }
   ]
}
{
   "_id" : 2,
   "item" : "pecans",
   "price" : 20,
   "quantity" : 1,
   "inventory_docs" : [
      { "_id" : 4, "sku" : "pecans", "description" : "product 4", "instock" : 70 }
   ]
}
{
   "_id" : 3,
   "inventory_docs" : [
      { "_id" : 5, "sku" : null, "description" : "Incomplete" },
      { "_id" : 6 }
   ]
}

 

MongoTemplate中實作的Java代碼

以下分別是Aggregation和Aggregates的實作

    /**
     * @Author zaoyu
     * Aggregation 實作$lookup
     */
    @Test
    public void testLookupAggregations(){
        String INVENTORY_COLLECTION = "inventory";
        String ORDERS_COLLECTION = "orders";
        // Aggregation類,直接可以呼叫lookup方法,傳入要關聯的表、當前表和關聯表關聯的欄位、要關聯的表的欄位、自定義名稱接收關聯匹配的資料
        LookupOperation lookup = Aggregation.lookup(INVENTORY_COLLECTION, "item", "sku", "inventory_docs");
        // 用newAggregation接收管道聚合指令,執行,得到結果,
        Aggregation aggregations =Aggregation.newAggregation(lookup);
        // mongoTemplate 直接呼叫aggregate方法,傳入Aggregation物件,基于的表,映射類(這里簡單化,我用Document)
        AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregations, ORDERS_COLLECTION, Document.class);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

    /**
     * @Author zaoyu
     * Aggregates/Bson 實作$lookup
     */
    @Test
    public void testLookupAggregates(){
        String INVENTORY_COLLECTION = "inventory";
        String ORDERS_COLLECTION = "orders";
        // 這里用Aggregates類直接呼叫lookup,傳入的引數和上面的Aggregations的lookup是一樣的,只不過這里回傳的結果是一個Bson物件,
        Bson lookupBson = Aggregates.lookup(INVENTORY_COLLECTION, "item", "sku", "inventory_docs");
        // 建一個List<Bson> 把lookupBson傳進去
        List<Bson> bsonList = new ArrayList<>();
        bsonList.add(lookupBson);
        // mongoTemplate先獲得對應的collection物件,然后呼叫aggregate,傳入List<Bson> 獲得結果
        MongoCollection<Document> collection = mongoTemplate.getCollection(ORDERS_COLLECTION);
        AggregateIterable<Document> resultList = collection.aggregate(bsonList);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

 

回傳結果

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}}]}}
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}}]}}
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,同時獲取分頁資料和總數,不用做兩次資料庫查詢(分別查詢分頁資料和總數),

 

語法

{ $facet:
   {
      <outputField1>: [ <stage1>, <stage2>, ... ],  // 這里outputpufield 是自己定義的用來接收stage集合回傳的檔案資料,  
      <outputField2>: [ <stage1>, <stage2>, ... ],  // 可以基于上一個Facet繼續做facet
      ...

   }
}

 

在MongoDB中操作的官方示例

// 資料  插入artwork 表中 
{ "_id" : 1, "title" : "The Pillars of Society", "artist" : "Grosz", "year" : 1926,
  "price" : NumberDecimal("199.99"),
  "tags" : [ "painting", "satire", "Expressionism", "caricature" ] }
{ "_id" : 2, "title" : "Melancholy III", "artist" : "Munch", "year" : 1902,
  "price" : NumberDecimal("280.00"),
  "tags" : [ "woodcut", "Expressionism" ] }
{ "_id" : 3, "title" : "Dancer", "artist" : "Miro", "year" : 1925,
  "price" : NumberDecimal("76.04"),
  "tags" : [ "oil", "Surrealism", "painting" ] }
{ "_id" : 4, "title" : "The Great Wave off Kanagawa", "artist" : "Hokusai",
  "price" : NumberDecimal("167.30"),
  "tags" : [ "woodblock", "ukiyo-e" ] }
{ "_id" : 5, "title" : "The Persistence of Memory", "artist" : "Dali", "year" : 1931,
  "price" : NumberDecimal("483.00"),
  "tags" : [ "Surrealism", "painting", "oil" ] }
{ "_id" : 6, "title" : "Composition VII", "artist" : "Kandinsky", "year" : 1913,
  "price" : NumberDecimal("385.00"),
  "tags" : [ "oil", "painting", "abstract" ] }
{ "_id" : 7, "title" : "The Scream", "artist" : "Munch", "year" : 1893,
  "tags" : [ "Expressionism", "painting", "oil" ] }
{ "_id" : 8, "title" : "Blue Flower", "artist" : "O'Keefe", "year" : 1918,
  "price" : NumberDecimal("118.42"),
  "tags" : [ "abstract", "painting" ] }


// 執行$facet聚合
db.artwork.aggregate( [
  {
    $facet: {
        // 第一個Facet操作,按照tag分類:先用unwind拆分tags欄位的陣列值,交給下一個聚合 $sortByCount, 按照tags的個數排序,
      "categorizedByTags": [
        { $unwind: "$tags" },
        { $sortByCount: "$tags" }
      ],
    // 第二個Facet操作,按照price分類:先過濾資料(只處理存在price資料的檔案),然后執行$bucket按照價格區間分組 0~150,151~200, 201~300, 301~400這樣,
      "categorizedByPrice": [
        { $match: { price: { $exists: 1 } } },
        {
          $bucket: {
            groupBy: "$price",
            boundaries: [  0, 150, 200, 300, 400 ],
            default: "Other",
            output: {
              "count": { $sum: 1 },
              "titles": { $push: "$title" }
            }
          }
        }
      ],
// 第三個Facet, 按照years分類, 分成4個區間,
      "categorizedByYears(Auto)": [
        {
          $bucketAuto: {
            groupBy: "$year",
            buckets: 4
          }
        }
      ]
    }
  }
])
    

 

MongoTemplate中實作的Java代碼

注:以下代碼的實作,資料來源參考上邊的官方示例的資料, artwork表, 請自行插入資料,

 1. 使用Aggregation物件實作

    /**
     * @Author zaoyu
     * Aggregation 實作$facet
     */
    @Test
    public void testFacetAggregations(){
        String ARTWORK_COLLECTION = "artwork";
        // Facet中第一組分類(categorizedByTags)的兩個聚合操作unwind 和 sortByCount
        UnwindOperation unwindForByTags = Aggregation.unwind("$tags");
        SortByCountOperation sortByCountForByTags = Aggregation.sortByCount("$tags");

        // Facet中第二組分類(categorizedByPrice)的聚合操作match 和 match
        MatchOperation matchForByPrice = Aggregation.match(Criteria.where("price").exists(true));
        // 分別傳入bucket分組的欄位price,設定區間值,并設定桶內條數統計和值(這里用titles接收title的值)
        BucketOperation bucketForByPrice = Aggregation.bucket("$price")
                .withBoundaries(0, 150, 200, 300, 400)
                .withDefaultBucket("Other")
                .andOutput("count").sum(1).as("count")
                .andOutput("$title").push().as("titles");

        // Facet中第三組分類 (categorizedByYears(Auto))的聚合操作,按年自動分成4個區間,
        BucketAutoOperation bucketForByYears = Aggregation.bucketAuto("$year", 4);

        // Aggregation呼叫facet方法,按照組別分類順序,把每一組的聚合操作和輸出的名稱傳進去,
        FacetOperation facetOperation = Aggregation.facet(unwindForByTags, sortByCountForByTags).as("categorizedByTags")
                .and(matchForByPrice, bucketForByPrice).as("categorizedByPrice")
                .and(bucketForByYears).as("categorizedByYears(Auto)");
        // 把facetOperation傳入newAggregation得到Aggregation物件,呼叫mongoTemplate的Aggregate方法執行得到結果
        Aggregation aggregation = Aggregation.newAggregation(facetOperation);
        AggregationResults<Document> resultList = mongoTemplate.aggregate(aggregation, ARTWORK_COLLECTION, Document.class);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

2. 使用Aggregates實作

  /**
     * @Author zaoyu
     * Aggregates 實作$facet
     */
    @Test
    public void testFacetAggregates() {
        String ARTWORK_COLLECTION = "artwork";
        // Facet中第一組分類(categorizedByTags)的兩個聚合操作unwind 和 sortByCount
        Bson unwindBsonForByTags = Aggregates.unwind("$tags");
        Bson sortByCountBsonForByTags = Aggregates.sortByCount("$tags");
        // 新建Facet物件,傳入第一組分類的接收名稱,以及在第一組分類中要做的聚合操作,
        Facet categorizedByTags = new Facet("categorizedByTags", unwindBsonForByTags, sortByCountBsonForByTags);

        // Facet中第二組分類(categorizedByPrice)的聚合操作match 和 match
        Bson matchBsonForPrice = Aggregates.match(Filters.exists("price"));
        // 這里面要新建BsonField構建 {"count": { $sum: 1 }  和 "titles": { $push: "$title" }} 作為第二組分類中$Bucket聚合操作中output值
        BsonField countOutput = new BsonField("count", new Document("$sum", 1));
        BsonField titleOutput = new BsonField("titles", new Document("$push", "$price"));
        // 上面2個操作傳入到BucketOption物件,最后傳到bucket操作
        BucketOptions bucketOptions = new BucketOptions().defaultBucket("Other").output(countOutput).output(titleOutput);
        Bson bucketBsonForByPrice = Aggregates.bucket("$price", Arrays.asList(0, 150, 200, 300, 400), bucketOptions);
        Facet categorizedByPrice = new Facet("categorizedByPrice", matchBsonForPrice, bucketBsonForByPrice);

        // Facet中第三組分類 (categorizedByYears(Auto))的聚合操作,按年自動分成4個區間,
        Bson bucketAutoBsonForByYears = Aggregates.bucketAuto("$year", 4);
        Facet categorizedByYears = new Facet("categorizedByYears", bucketAutoBsonForByYears);

        // 新建一個List<Facet>把每組分類的Facet物件傳進去,
        List<Facet> facetList = new ArrayList<>();
        facetList.add(categorizedByTags);
        facetList.add(categorizedByPrice);
        facetList.add(categorizedByYears);
        // 呼叫Aggregates的facet方法,傳入List<Facet>得到最終Bson物件,并添加到Bson集合中,
        Bson facetBson = Aggregates.facet(facetList);
        List<Bson> bsonList = new ArrayList<>();
        bsonList.add(facetBson);
        // 呼叫方法執行得到結果
        MongoCollection<Document> collection = mongoTemplate.getCollection(ARTWORK_COLLECTION);
        AggregateIterable<Document> resultList = collection.aggregate(bsonList);
        for (Document document : resultList) {
            System.out.println("result is :" + document);
        }
    }

最侄訓傳結果, 二者一樣,

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之間的轉換和獲取, 

 

希望這篇文章能幫到大家,有錯漏之處,歡迎指正, 

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/539744.html

標籤:NoSQL

上一篇:《MySQL必知必會》之快速入門游標和觸發器

下一篇:SpringBoot中MongoDB聚合管道查詢操作$facet$lookup$unwind$group

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more