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放在前面,先查找出符合条件的数据再进行连表查询,这样既可以提高查询效率,又可以避免字段类型问题。

总结

   目前用到的就是这样的操作,往后还有更复杂的操作时再继续更新记录内容。

原文地址:https://www.cnblogs.com/xuanhaoo/p/14335376.html