MongoTemplate聚合(一)$lookup


mongodb

   最近入職了新的公司,新公司統一使用的mongodb,es等非關系型數據庫。以前對es有一些了解,其實就是靈活的文檔類型結構,不受限於關系型數據庫的那種字段唯一確定的”死板“,但是無論是關系型數據庫還是非關系型數據庫,目前使用了一段時間來說,我認為各有優劣,具體選擇要結合業務場景來進行選擇。

   有關mongo的快速學習文檔可以參照以下資料來學習:

聚合查詢

   在我這種習慣了mysql這種關系型數據的結構設計中,來處理mongo集合(數據表)的一些操作票,或多或少還是受到關系型數據庫思想的影響與約束,畢竟還是使用了這么多年了。。。
   比如在下面這種場景下:A對象集合與B對象集合之間有關聯關系,此時,針對於上級關系修改操作較少的可以將他們之間的關系映射成嵌入式的子文檔,但他們的數據都在經常性的發生相互變化,這種情況很顯然不能將數據作為嵌入式文檔保存,應該要實時的查詢關聯的數據。
   mongo中早期的一些版本又沒有left join,right join的概念,后來在3.2版本開始,增加了$lookup操作。

   在介紹$lookup前簡單了解一下mongo中的一些聚合管道操作:

聚合指令 功能描述
$match 篩選,選擇要處理的文檔
$project 指定輸出文檔中的字段,映射別名等
$group 顧名思義,分組 根據指定內容來分組
$limit 限制傳遞到下一步的文檔數量
$skip 跳過當前順序的一定數量的文檔
$unwind 擴展數組,為每一個3數組入口生成一個輸出文檔
$sort 文檔排序
$lookup 多表關聯(since 3.2+)
$geoNear 選擇某個地理位置附近的的文檔
$out 把管道的結果寫入某個集合
$redact 控制特定數據的訪問

   上面的指定都屬於聚合管道中的操作,(官方解釋)聚合管道是用於數據聚合的框架,其模型基於數據處理管道的概念。文檔進入多階段管道,將文檔轉換為聚合結果。MongoDB 聚合管道由多個階段組成。每個階段在文檔通過管道時轉換文檔。管道階段不需要為每個輸入文檔生成一個輸出文檔; 如:某些階段可能會生成新文檔或過濾掉文檔。后邊有時間寫一篇文章來記錄一下聚合管道。

特別說明 - 局限性

    mongodb的官方文檔說明:$lookup: Performs a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing. The $lookup stage does an equality match between a field from the input documents with a field from the documents of the “joined” collection.

簡單點說就是: $lookup只能在同一個數據庫中, 且這個collection不能有分片. 如果你的集合設計不在一個庫中, 且設置了分片的話, 那下面的連表操作都是無效的,請不用浪費時間瀏覽了。

$lookup關聯

   $lookup是管道中的一個階段,在這個階段,可以做連表操作,具有如下語法:

{
   $lookup:
     {
       from: <collection to join 需要左連接的集合>,
       localField: <主集合中與該左連集合關聯的字段>,
       foreignField: <左連集合中對應的字段>,
       as: <output array field 指定新輸入數組字段的名稱:該處會處理成數組>
     }
}

該操作等效於如下sql釋義:

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

mongoTemplate中使用

   說了那么多,是想介紹一下簡單的概念,言歸正傳,開始講在springboot中使用mongoTemplate中該如何使用。
   假設當前有兩個集合,一個company,一個product,產品隸屬於公司下,他們之間存在關聯關系。
   1. CompanyMongoPO.java


import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.mongodb.core.mapping.Document;


@Data
@Accessors(chain = true)
@Document("company")
public class CompanyMongoPO {

    @Id
    private String id;

    private String name;

}

   2. productMongoPO.java

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.mongodb.core.mapping.Document;


@Data
@Accessors(chain = true)
@Document("product")
public class ProductMongoPO {

    @Id
    private String id;

    private String name;

    private String companyId;

}

   3. 在Dao層進行查詢操作:

......
		 Criteria criteria = new Criteria();
		 criteria.and("companyId").is("companyId");
		 // 構造聚合管道操作
        List<AggregationOperation> operationList = Lists.newArrayList();
        // 這一步很重要,將product中的companyId字段轉化為ObjectId類型,因為String類型和ObjectId類型不一樣,會導致連接失效
        AddFieldsOperation addFieldsOperation = AddFieldsOperation.addField("companyId").withValue(ConvertOperators.ToObjectId.toObjectId("$companyId")).build();
        LookupOperation companyLookupOperation = LookupOperation.newLookup()
                .from("company")
                .localField("companyId")
                .foreignField("_id")
                .as("companyJoin");
        AggregationOperation match = Aggregation.match(criteria);
        ProjectionOperation project = Aggregation.project("id","name","companyId")
                                    .and("companyJoin.name").as("companyName");
        // 分頁與排序操作,字段未在上面體現出來
        SkipOperation skip = Aggregation.skip((long)param.getOffset());
        LimitOperation limit = Aggregation.limit(param.getPageSize());
        SortOperation sort = Aggregation.sort(Sort.Direction.DESC, "createTime");
        // 封裝條件:此處的順序可以調整 將match放到前面可以避免因為AddFiled引起的companyId字段類型變化
        operationList.add(match);
        operationList.add(addFieldsOperation);
        operationList.add(companyLookupOperation);
        operationList.add(project);
        operationList.add(sort);
        operationList.add(skip);
        operationList.add(limit);

        Aggregation agg = Aggregation.newAggregation(operationList);
        AggregationResults<ProductItemVO> aggregationResults = this.getMongoTemplate().aggregate(agg, "product", ProductItemVO.class);
        List<ProductItemVO> dataList = aggregationResults.getMappedResults();
......

   如上偽代碼,就是一個簡單的聚合操作,當然有幾個地方需要注意一下:

  • 連表時的字段類型要一致:比如上面的companyId和id字段,一個String,一個是ObjectId類型,需要將companyId轉化為ObjectId類型,再進行連接,當然這個地方我理解也可以將_id轉化為String類型,但是經過我的測試,發現沒有成功,還需要找下原因。(更新:沒有成功的原因是顛倒了查詢主表和連接從表的先后順序)

  • 在將companyId轉化為ObjectId類型后,如果后面有使用到companyId作為match的篩選條件字段,這個地方要注意一下,在聚合管道中,有一定的順序性,如果將AddFieldsOperation操作放在match之前,那么會導致match這個字段的條件失效,需要調整一下順序,將match放在前面,先查找出符合條件的數據再進行連表查詢,這樣既可以提高查詢效率,又可以避免字段類型問題。

總結

   目前用到的就是這樣的操作,往后還有更復雜的操作時再繼續更新記錄內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM