Mongo学习笔记(三) 通过Aggregation和lookup进行多级关联查询

第三篇笔记本想着记录一些简单的增删改查,由于中间很久没有写就一时懒得整理了,先把最近刚遇到的问题记录一下

通过Aggregation和lookup进行多级关联查询

在SQL中可以通过left join操作进行多表的关联查询,在mongo中,类似的操作为Aggregation中的lookup,可以看一下如下数据结构

@Data
public class Subject {

    private ObjectId id;
    private String name;
    private String title;
    private String desc;
}

@Data
public class Topic {

    private String id;
    private String name;
    private String subjectId;
    private int order;
    private TopicType type;
}

@Data
public class TopicOption {

    private String id;
    private String optionDesc;
    private String topicId;
    private int order;
    private String optionValue;


}

mongoTemplate查询语句

可以看出,Subject的id和Topic中的subjectId关联,Topic中的id和TopicOption中的topicId关联,在这里当初写代码时留下了一个坑,就是所有的Id都使用了默认的ObjectId,而在外键中使用了String进行保存,在进行lookup时就会出现一个问题,字段类型不同,无法进行关联,需要先对id进行处理,转为String,Mongo的查询语句如下

db.subject.aggregate([
    {$project:{id:{ $toString: '$_id' },title:1}},
    {
        $lookup:
        {
            from: "topic",
            localField: "id",
            foreignField: "subjectId",
            as: "topics"
        }
    },
    {$unwind:"$topics"},
    {
        $project:{

            title:1,
            topics:{
                id:{$toString: '$topics._id'} ,
                name:1,
                type:1,
                order:1
            }
        }
    },
    {
        $lookup:
        {
            from: "topicOption",
            localField: "topics.id",
            foreignField: "topicId",
            as: "topics.topicOptions"
        }
    },
    {
        $group:{
            _id:   "$_id",
            title:{$first:'$title'},
            topics:{$push:'$topics'}
        }
    }
    ])   ;

这里有两个地方需要注意

  1. 第一个lookup后使用了unwind将单个Bson拆为Bson数组,这点不可缺少,不然第二层lookup会关联不上
  2. 这里使用了比较蠢的方法project来将ObjectId转为String,后来发现其实有个更简单的pipeline addFields,这一点会在java代码中进行说明,这里作为学习历程就留着了
  3. 因为之前使用了unwind,最后使用了group再进行一次压缩聚合

Java实现

SpingData中提供的API

根据以上脚本

Aggregation aggregation = Aggregation.newAggregation(
               Aggregation.match(where("_id").is(subjectId)),
               Aggregation.project("title").andExpression("toString(_id)").as("id"),
               Aggregation.lookup(Fields.field("topic"),Fields.field("id"),Fields.field("subjectId"),Fields.field("topics")),
               Aggregation.unwind("topics"),
            
              Aggregation.project("title","id")
                       .andExpression("toString(topics._id)").as("topics.id")
                       .and("topics.name").as("topics.name")
                       .and("topics.order").as("topics.order")
                       .and("topics.type").as("topics.type"),
               Aggregation.lookup("topicOption","topics.id","topicId","topics.topicOptions"),
               Aggregation.group("id").first("title").as("title").push("topics").as("topics")
       );

一直在group之前该部分代码都达到了预期效果,但是在添加了group之后,抛出了如下异常

java.lang.IllegalArgumentException: Invalid reference 'topics'!

上来感觉很纳闷,明明有topics字段,为什么解析不到,经过多次尝试,发现是第二个project中未声明topics,导致解析报错(不是Mongo的报错,是Aggregation自检过程中的报错)。后来经过查阅Mongo的官方文档,发现如果只是单纯的添加字段,其实有addFields这么一个简单好用的pipeline,调整后的代码如下

Aggregation aggregation = Aggregation.newAggregation(
               Aggregation.match(where("_id").is(subjectId)),
               Aggregation.project("title").andExpression("toString(_id)").as("id"),
               Aggregation.lookup(Fields.field("topic"),Fields.field("id"),Fields.field("subjectId"),Fields.field("topics")),
               Aggregation.unwind("topics"),
               Aggregation.addFields().addField("topics.id").withValueOfExpression("toString(topics._id)").build(),
              /* Aggregation.project("title","topics.name","topics.type","topics.order")
                       .andExpression("toString(topics._id)").as("topics.id")*/
          /*     Aggregation.project("title","id")
                       .andExpression("toString(topics._id)").as("topics.id")
                       .and("topics.name").as("topics.name")
                       .and("topics.order").as("topics.order")
                       .and("topics.type").as("topics.type"),*/
               Aggregation.lookup("topicOption","topics.id","topicId","topics.topicOptions"),
               Aggregation.group("id").first("title").as("title").push("topics").as("topics")
       );

第一个project其实也应该修改为addFields,但是这里想给大家一个project的写法参考,就暂时留在了这里

mongClient中提供的API

先补充一个数据结构

@Data
public class Answer {

    private String id;
    private String subjectId;
    private String topicId;
    private String topicValue;
    private String regOper;


}

查询语句也有小改动

db.subject.aggregate([
    {$match:{_id:{$eq:ObjectId('5f363791badd872947095089')}}},
    {$addFields:{id:{ $toString: '$_id' }}},
    {
        $lookup:
        {
            from: "topic",
                    localField: "id",
                foreignField: "subjectId",
                as: "topics"
        }
    },
    {$unwind:"$topics"},
    {$addFields:{'topics.id':{ $toString: '$topics._id' }}},
    {
        $lookup:
        {
            from: "answer",
                    localField: "topics.id",
                foreignField: "topicId",
                as: "topics.answer"
        }
    }
    ])  

java代码

 MongoCollection<Document> collection= mongoOps.getCollection("subject");
        List<Document> results= new ArrayList<>();
        collection.aggregate(
                Arrays.asList(
                        Aggregates.match(Filters.eq("_id", new ObjectId(subjectId))),
                        Aggregates.addFields(new Field<>("id",new Document("$toString","$_id"))),
                        Aggregates.lookup("topic","id","subjectId","topics"),
                        Aggregates.unwind("$topics"),
                        Aggregates.addFields(new Field<>("topics.id",new Document("$toString","$topics._id"))),
                        Aggregates.lookup("answer","topics.id","topicId","topics.answer"),
                        Aggregates.group("$_id",Accumulators.first("title","$title"),
                                Accumulators.push("topics","$topics"))

                )
        ).forEach( t -> results.add(t));

使用原生的API时,如果通过自动生成的_id去查询,记得使用数据类型ObjectId,使用mongoTemplate应该是会自动进行转换

小结

相较于SQL中的left join,感觉mongo的lookup要多写出来很多代码,特别是多层嵌套时更是感觉不便;但是相对的,mongo的语法更加的结构化,通过pipeline将所有的步骤穿在一起,在编写代码时感觉也更加整洁。同时也提醒大家,在设计数据结构时,关联字段类型一定要一致,特别是ObjectId和String,在java代码层面感受不到区别,但是在实际落表后确是完完全全不同,由此在处理时也会带来一系列的不便
最后附上代码地址https://gitee.com/xiiiao/mongo-learning

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在Spring Data MongoDB中,可以使用MongoDB的聚合管道(Aggregation)和lookup操作将两个集合关联起来。 假设我们有两个集合:orders和customers,orders集合包含每个订单的信息,而customers集合包含每个客户的信息。我们希望在查询订单时,将每个订单的客户信息也一起查询出来。 可以使用聚合管道中的$lookup操作实现这个需求。具体步骤如下: 1. 创建一个Orders类来表示订单,其中包含客户ID字段: ``` @Document(collection = "orders") public class Order { @Id private String id; private String customerId; private double amount; // getters and setters } ``` 2. 创建一个Customers类来表示客户,其中包含客户ID和客户名字字段: ``` @Document(collection = "customers") public class Customer { @Id private String id; private String name; // getters and setters } ``` 3. 创建一个OrderWithCustomer类来表示订单和客户信息,其中包含Order和Customer对象: ``` public class OrderWithCustomer { private Order order; private Customer customer; // getters and setters } ``` 4. 使用聚合管道查询订单和客户信息: ``` @Autowired private MongoTemplate mongoTemplate; public List<OrderWithCustomer> findOrdersWithCustomers() { LookupOperation lookupOperation = LookupOperation.newLookup() .from("customers") .localField("customerId") .foreignField("id") .as("customer"); Aggregation aggregation = Aggregation.newAggregation( lookupOperation, Aggregation.unwind("customer"), Aggregation.project() .and("customer._id").as("customer.id") .and("customer.name").as("customer.name") .and("amount").as("order.amount") .andExclude("_id"), Aggregation.replaceRoot().withValueOf(ObjectOperators.valueOf("order")) ); AggregationResults<OrderWithCustomer> results = mongoTemplate.aggregate(aggregation, "orders", OrderWithCustomer.class); return results.getMappedResults(); } ``` 在聚合管道中,我们首先使用$lookup操作将orders集合中的customerId字段和customers集合中的_id字段进行关联,得到每个订单对应的客户信息。然后使用$unwind操作将客户信息拆分成多个文档,方便后续处理。接着使用$project操作将客户信息和订单信息合并到一个文档中。最后使用$replaceRoot操作将合并后的文档替换为原来的订单文档,得到最终的查询结果。 这样,我们就可以通过聚合管道和lookup操作实现两个集合的关联查询
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值