前言

一对一、一对多、多对多,作为 NoSQL 领头羊的 MongoDB 中常用做法无非「内嵌」和「引用」两种,因为 Document 有 16MB 的大小限制且「内嵌」不适合复杂的多对多关系,「引用」是用得更广泛的关联方式,所以 MongoDB 官方称其为“Normalized Data Models”——标准化数据模型。
引用式的关联其实很简单,指文档与文档之间通过id字段的引用来进行关联,下图是在文章表中包含了分类和标签表中的id字段,作为外键存在
image.png

这里是三张不同的表:文章、分类、标签,mongoose的id会自动生成

下面我们会根据这张图中的关系分别使用populate和aggregate的方式进行查询

模拟数据

首先新增分类和标签模型内容

  1. // 创建分类模型
  2. const CategorySchema = new mongoose.Schema({
  3. name: { type: String }
  4. })
  5. // 创建标签模型
  6. const TagsSchema = new mongoose.Schema({
  7. name: { type: String }
  8. })
  1. // 新增分类
  2. await Category.insertMany([
  3. { name: 'nodejs' },
  4. { name: 'vuejs' },
  5. ])
  6. await Tags.insertMany([
  7. { name: 'JavaScript' },
  8. { name: 'vue' },
  9. { name: 'react' },
  10. { name: 'html' },
  11. { name: 'css' },
  12. ])

新增文章并模拟数据关联

  1. await Article.insertMany([
  2. { title: '博客文章1', body: '内容1', cid: "634144ba02bb3f9e44b7b3ae", tid: "634144d3ec1d411217edf8f1" },
  3. { title: '博客文章2', body: '内容2', cid: "634144ba02bb3f9e44b7b3af", tid: ["634144d3ec1d411217edf8f2", "634148e9aa35256326ce8f6a"] }
  4. ])

关联查询

Populate

官方解释:

MongoDB在版本>= 3.2中有类似于联接的$查找聚合运算符。Mongoose有一个更强大的替代方案,称为populate(),它允许您引用其他集合中的文档。填充是自动用来自其他集合的文档替换文档中的指定路径的过程。我们可以填充单个文档、多个文档、一个普通对象、多个普通对象,或者从查询返回的所有对象

Mongoose的一切始于Schema,使用populate的重点也在于Schema中的设置

  1. const ArticleSchema = new mongoose.Schema({
  2. title: { type: String },
  3. body: { type: String },
  4. author: { type: String },
  5. cid: {
  6. type: mongoose.Schema.Types.ObjectId,
  7. ref: "Category"
  8. },
  9. tid: [
  10. {
  11. type: mongoose.Schema.Types.ObjectId,
  12. ref: "Tags"
  13. }
  14. ]
  15. })

这里是Article模型
下面在接口中使用populate()

  1. router.get(`/api/article/article_list`, async (req, res) => {
  2. const result = await Article.find({}).populate('cid').populate('tid')
  3. res.json({
  4. code: CODE_SUCCESS,
  5. msg: "查询成功",
  6. data: {
  7. result
  8. }
  9. })
  10. })

返回的结果数据:

  1. {
  2. "code": 0,
  3. "msg": "查询成功",
  4. "data": {
  5. "result": [
  6. {
  7. "_id": "63414ae914a4ebfba8a14bf0",
  8. "title": "博客文章1",
  9. "body": "内容1",
  10. "cid": {
  11. "_id": "634144ba02bb3f9e44b7b3ae",
  12. "name": "nodejs",
  13. "__v": 0
  14. },
  15. "tid": [
  16. {
  17. "_id": "634144d3ec1d411217edf8f1",
  18. "name": "JavaScript",
  19. "__v": 0
  20. }
  21. ],
  22. "__v": 0
  23. },
  24. {
  25. "_id": "63414ae914a4ebfba8a14bf2",
  26. "title": "博客文章3",
  27. "body": "内容3",
  28. "cid": {
  29. "_id": "634144ba02bb3f9e44b7b3af",
  30. "name": "vuejs",
  31. "__v": 0
  32. },
  33. "tid": [
  34. {
  35. "_id": "634144d3ec1d411217edf8f2",
  36. "name": "vue",
  37. "__v": 0
  38. },
  39. {
  40. "_id": "634148e9aa35256326ce8f6a",
  41. "name": "react",
  42. "__v": 0
  43. }
  44. ],
  45. "__v": 0
  46. }
  47. ]
  48. }
  49. }

aggregate

作为MongoDB官方推荐聚合查询方式,而要关联多个集合需要使用$lookup,如果一篇文章可以有多个标签(tags),划分到多个分类(categories),作为一对多的关联关系
使用aggregate的话不再需要指向对应的模型

  1. cid: {
  2. type: mongoose.Schema.Types.ObjectId,
  3. },
  4. tid: [
  5. {
  6. type: mongoose.Schema.Types.ObjectId,
  7. }
  8. ]

进行聚合查询

  1. router.get(`/api/article/article_list`, async (req, res) => {
  2. const result = await Article.aggregate([
  3. {
  4. $lookup: {
  5. from: 'categories', // 数据库中关联的集合名称
  6. localField: 'cid', // article模型中关联的字段
  7. foreignField: '_id', // 需要关联categories中的字段
  8. as: 'categoryList' // 返回数据的别名字段
  9. },
  10. },
  11. {
  12. $lookup: {
  13. from: 'tags', // 数据库中关联的集合名称
  14. localField: 'tid', // article模型中关联的字段
  15. foreignField: '_id', // 需要关联tags中的字段
  16. as: 'tagList' // 返回数据的别名字段
  17. },
  18. },
  19. {
  20. $match: {} // 这里是查询时的筛选条件,查询全部设置为空,或者不用设置
  21. }
  22. ])
  23. res.json({
  24. code: CODE_SUCCESS,
  25. msg: "查询成功",
  26. data: {
  27. result
  28. }
  29. })
  30. })

返回数据结果

  1. {
  2. "code": 0,
  3. "msg": "查询成功",
  4. "data": {
  5. "result": [
  6. {
  7. "_id": "63423143d78882b9d887224d",
  8. "title": "博客文章1",
  9. "body": "内容1",
  10. "cid": "634144ba02bb3f9e44b7b3ae",
  11. "tid": [
  12. "634144d3ec1d411217edf8f1"
  13. ],
  14. "__v": 0,
  15. "categoryList": [
  16. {
  17. "_id": "634144ba02bb3f9e44b7b3ae",
  18. "name": "nodejs",
  19. "__v": 0
  20. }
  21. ],
  22. "tagList": [
  23. {
  24. "_id": "634144d3ec1d411217edf8f1",
  25. "name": "JavaScript",
  26. "__v": 0
  27. }
  28. ]
  29. },
  30. {
  31. "_id": "63423143d78882b9d887224f",
  32. "title": "博客文章2",
  33. "body": "内容2",
  34. "cid": "634144ba02bb3f9e44b7b3af",
  35. "tid": [
  36. "634144d3ec1d411217edf8f2",
  37. "634148e9aa35256326ce8f6a"
  38. ],
  39. "__v": 0,
  40. "categoryList": [
  41. {
  42. "_id": "634144ba02bb3f9e44b7b3af",
  43. "name": "vuejs",
  44. "__v": 0
  45. }
  46. ],
  47. "tagList": [
  48. {
  49. "_id": "634148e9aa35256326ce8f6a",
  50. "name": "react",
  51. "__v": 0
  52. },
  53. {
  54. "_id": "634144d3ec1d411217edf8f2",
  55. "name": "vue",
  56. "__v": 0
  57. }
  58. ]
  59. }
  60. ]
  61. }
  62. }

参考链接:
MongoDB 中的关联查询