第三篇笔记本想着记录一些简单的增删改查,由于中间很久没有写就一时懒得整理了,先把最近刚遇到的问题记录一下
在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;
}
可以看出,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'}
}
}
]) ;
这里有两个地方需要注意
unwind
将单个Bson拆为Bson数组,这点不可缺少,不然第二层lookup会关联不上project
来将ObjectId
转为String
,后来发现其实有个更简单的pipeline addFields
,这一点会在java代码中进行说明,这里作为学习历程就留着了unwind
,最后使用了group
再进行一次压缩聚合根据以上脚本
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的写法参考,就暂时留在了这里
先补充一个数据结构
@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