副表关联主表主键: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");
1
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);
1
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
});
1
2
3
4
5

populate(path, select)

填充 document

let author = await Author.findOne({ name: "dora" }).populate("story");

author.story; // {...} 从 Story 表中查到的文档
1
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; // []
1
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
1
2
3
4
5
6
7

populate 多个字段

let author = await Author.findOne({ name: "dora" })
  .populate("story")
  .populate("friends");
1
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" },
});
1
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,
});
1
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);
1
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",
});
1
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; // "笑场中的经典语录..."
1
2
3

当然在 commentSchema 中也可以定义单独的 blogPost 和 product 字段,分别存储 _id 和对应的 ref 选项。 但是,这样是不利于业务扩展的,比如在后续的业务中增加了歌曲或电影的用户评论,则需要在 schema 中添加更多相关字段。而且每个字段都需要一个 populate() 查询。而使用 refPath 意味着,无论 commentSchema 可以指向多少个 Model,联合查询的时候只需要一个 populate() 即可。 参考open in new window

Last Updated:
Contributors: websong