副表关联主表主键:populate 跨集合查询
导读
Mongoose 的 populate() 可以连表查询,即在另外的集合中引用其文档。
Populate() 可以自动替换 document 中的指定字段,替换内容从其他 collection 中获取。
示例
ref
创建 Model 的时候,可给该 Model 中关联存储其它集合 _id 的字段设置 ref 选项。ref 选项告诉 Mongoose 在使用 populate() 填充的时候使用哪个 Model。
// 学生集合【主集合】这个即便不使用,也要声明出来,否则报错
let userSchema = new mongoose.Schema(
{
name: String,
age: Number,
},
{ collection: "User" }
);
connetction.model("User", userSchema);
// 成绩集合【附属集合】
let scoreSchema = new mongoose.Schema({
// 定义外键,关联上userSchema的id值,类型是ObjectId类型,引用的是User集合模型;
uid: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
grade: Number, // 自有字段
});
let Score = connetction.model("Score", scoreSchema);
// 插入数据
// 1. 先创建学生,再插入分数
(async function() {
let user = await User.create({ name: "宋宇" }); // 先创建主表
let score = await Score.create({ uid: user._id, grade: 100 });
console.log(user);
console.log(score);
})();
// 2. 通过分数文档id查用户信息
(async function(scoreId) {
// populate填充的意思,就是把一个外键字段从一个ObjectId变成一个对象;
const score = await Score.findById(scoreId).populate("uid");
console.log(score);
})("5edcb62808896f3dac072513");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
以下摘自 segmentfault
refs
创建 Model 的时候,可给该 Model 中关联存储其它集合 _id 的字段设置 ref 选项。ref 选项告诉 Mongoose 在使用 populate() 填充的时候使用哪个 Model。
const authorSchema = new Schema({
name: String,
age: Number,
story: { type: Schema.Types.ObjectId, ref: 'Story' }
friends: [{ type: Schema.Types.ObjectId, ref: 'User' }]
});
let Author = mongoose.model('Author', authorSchema);
2
3
4
5
6
7
8
上例中 Author model 的 friends 字段设为 ObjectId 数组。 ref 选项告诉 Mongoose 在填充的时候使用 User model。所有储存在 friends 中的 _id 都必须是 User model 中 document 的 _id。
ObjectId、Number、String 以及 Buffer 都可以作为 refs 使用。 但是最好还是使用 ObjectId。
在创建文档时,保存 refs 字段与保存普通属性一样,把 _id 的值赋给它就好了。
Author.create({
name: "dora",
age: 18,
story: story._id, // 直接赋值 story 的 _id
});
2
3
4
5
populate(path, select)
填充 document
let author = await Author.findOne({ name: "dora" }).populate("story");
author.story; // {...} 从 Story 表中查到的文档
2
3
被填充的 story 字段已经不是原来的 _id,而是被指定的 document 代替。这个 document 由另一条 query 从数据库返回。
refs 数组返回存储对应 _id 的 document 数组。
没有关联的 document
如果没有关联的文档,则返回值为 null,即 author.story 为 null;如果字段是数组,则返回 [] 空数组即 author.friends 为 []。
let author = await Author.findOne({ name: "dora" }).populate("friends");
author.friends; // []
2
3
返回字段选择
如果只需要填充 document 中一部分字段,可给 populate() 传入第二个参数,参数形式即 返回字段字符串,同 Query.prototype.select()。
let author = await Author.findOne({ name: "dora" }).populate(
"story",
"title -_id"
);
author.story; // {title: ...} 只返回 title 字段
author.story.content; // null 其余字段为 null
2
3
4
5
6
7
populate 多个字段
let author = await Author.findOne({ name: "dora" })
.populate("story")
.populate("friends");
2
3
如果对同一字段 populate() 两次,只有最后一次生效。
populate({ objParam })
objParam:
- path:需要 populate 的字段。
- populate:多级填充。
- select:从 populate 的文档中选择返回的字段。
- model:用于 populate 的关联 model。如果没有指定,populate 将根据 schema 中定义的 ref 字段中的名称查找 model。可指定跨数据库的 model。
- match:populate 连表查询的条件,符合条件的会用文档替换 _id,不符合条件的会用 null 替换 _id。
- options:populate 查询的选项。
- sort:排序。
- limit:限制数量。
多级填充
// 查询 friends 的 friends
Author.findOne({ name: "dora" }).populate({
path: "friends",
populate: { path: "friends" },
});
2
3
4
5
跨数据库填充
跨数据库不能直接通过 schema 中的 ref 选项填充,但是可以通过 objParam 中的 model 选项显式指定一个跨数据库的 model。
let eventSchema = new Schema({
name: String,
conversation: ObjectId, // 注意,这里没有指定 ref!
});
let conversationSchema = new Schema({
numMessages: Number,
});
let db1 = mongoose.createConnection("localhost:27000/db1");
let db2 = mongoose.createConnection("localhost:27001/db2");
// 不同数据库的 Model
let Event = db1.model("Event", eventSchema);
let Conversation = db2.model("Conversation", conversationSchema);
// 显示指定 model
let doc = await Event.find().populate({
path: "conversation",
model: Conversation,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过 refPath 动态引用填充的 Model
Mongoose 还可以针对同一个存储 _id 的字段从多个不同的集合中查询填充。
//用于存储评论的 schema。用户可以评论博客文章或作品。
const commentSchema = new Schema({
body: { type: String, required: true },
on: {
type: Schema.Types.ObjectId,
required: true,
refPath: "onModel",
},
onModel: {
type: String,
required: true,
enum: ["BlogPost", "Product"],
},
});
const Product = mongoose.model("Product", new Schema({ name: String }));
const BlogPost = mongoose.model("BlogPost", new Schema({ title: String }));
const Comment = mongoose.model("Comment", commentSchema);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
refPath 选项比 ref 更复杂。 如果 ref 只是一个字符串,Mongoose 将查询相同的 model 以查找填充的子文档。 而使用 refPath,可以配置用于每个不同文档的 model。
const book = await Product.create({ name: "笑场" });
const blog = await BlogPost.create({
title: "笑场中的经典语录,句句犀利,直戳人心",
});
// 分别指定了不同评论来源的两个评论数据
const commentOnBook = await Comment.create({
body: "Bravo",
on: book._id,
onModel: "Product",
});
const commentOnBlog = await Comment.create({
body: "未曾开言我先笑场。笑场完了听我诉一诉衷肠。",
on: blog._id,
onModel: "BlogPost",
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const comments = await Comment.find().populate("on");
comments[0].on.name; // "笑场"
comments[1].on.title; // "笑场中的经典语录..."
2
3
当然在 commentSchema 中也可以定义单独的 blogPost 和 product 字段,分别存储 _id 和对应的 ref 选项。 但是,这样是不利于业务扩展的,比如在后续的业务中增加了歌曲或电影的用户评论,则需要在 schema 中添加更多相关字段。而且每个字段都需要一个 populate() 查询。而使用 refPath 意味着,无论 commentSchema 可以指向多少个 Model,联合查询的时候只需要一个 populate() 即可。 参考open in new window