Mongoose 實現關聯查詢和踩坑記錄

語言: CN / TW / HK

「深度學習福利」大神帶你進階工程師,立即檢視>>>


本文源自工作中的一個問題,在使用 Mongoose 做關聯查詢時發現使用 populate() 方法不能直接關聯非 _id 之外的其它欄位,在網上搜索時這塊的解決方案也並不是很多,在經過一番查閱、測試之後,有兩種可行的方案,使用 Mongoose 的 virtual 結合 populate 和 MongoDB 原生提供的 Aggregate 裡面的 $lookup 階段來實現。

文件內嵌與引用模式

MongoDB 是一種文件物件模型,使用起來很靈活,它的文件結構分為 內嵌和引用 兩種型別。

內嵌是把相關聯的資料儲存在同一個文件內,我們可以用物件或陣列的形式來儲存,這樣好處是我們可以在一個單一操作內完成,可以傳送較少的請求到資料庫服務端,但是這種內嵌型別也是一種冗餘的資料模型,會造成資料的重複,如果很複雜的一對多或多對多的關係,表達起來就很複雜,也要注意內嵌還有一個最大的單條文件記錄限制為 16MB。

引用模型是一種規範化的資料模型,通過主外來鍵的方式來關聯多個文件之間的引用關係,減少了資料的冗餘,在使用這種資料模型中就要用到關聯查詢,也就是本文我們要講解的重點。

圖片來源:mongoing[1]


引用模型示例


JSON 模型

我們通過作者和書籍的關係,一個作者對應多個書籍這樣一個簡單的示例來學習如何在 MongoDB 中實現關聯非 _id 查詢。

  • Author
{
  "bookIds":[
      26351021,
      26854244,
      27620408
  ],
  "authorId":1,
  "name":"Kyle Simpson"
}
  • Book
[
  {
    "bookId":26351021,
    "name":"你不知道的JavaScript(上卷)",
  },
  {
    "bookId":26854244,
    "name":"你不知道的JavaScript(中卷)",
  },
  {
    "bookId":27620408,
    "name":"你不知道的JavaScript(下卷)",
  }
]


定義 Schema

使用 Mongoose 第一步要先定義集合的 Schema。

  • author.js

建立 model/author.js 定義作者的 Schema,程式碼中的 ref 表示要關聯的 Model 是誰,在 Schema 定義好之後後面我會建立 Model

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const AuthorSchema = new Schema({
  authorIdNumber,
  nameString,
  bookIds: [{ typeNumberref'Books' }]
});
AuthorSchema.index({ authorId1}, { uniquetrue });

module.exports = AuthorSchema;
  • book.js

建立 model/book.js 定義書籍的 Schema。

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const BookSchema = new Schema({
  bookIdNumber,
  nameString,
});
BookSchema.index({ bookId1}, { uniquetrue });

module.exports = BookSchema;
  • index.js

建立 model/index.js 定義 Model 和連結資料庫。

const mongoose = require('mongoose');
const AuthorSchema = require('./author');
const BookSchema = require('./book');

const DB_URL = process.env.DB_URL;
const AuthorModel = mongoose.model('Authors', AuthorSchema, 'authors');
const BookModel = mongoose.model('Books', BookSchema, 'books');

mongoose.set('useCreateIndex'true)
mongoose.connect(DB_URL, {useNewUrlParsertrueuseUnifiedTopologytrue});

module.exports = {
  AuthorModel,
  BookModel,
}


使用 Aggregate 的 $lookup 實現關聯查詢

MongoDB 3.2 版本新增加了 $lookup 實現多表關聯,在聚合管道階段中使用,經過 $lookup 階段的處理,輸出的新文件中會包含一個新生成的陣列列。

建立一個 aggregateTest.js 重點在於 $lookup 物件,程式碼如下所示:

  • $lookup.from: 在同一個資料庫中指定要 Join 的集合的名稱。
  • $lookup.localFiled: 關聯的源集合中的欄位,本示例中是 Authors 表的 authorId 欄位。
  • $lookup.foreignFiled: 被 Join 的集合的欄位,本示例中是 Books 表的 bookId 欄位。
  • $as: 別名,關聯查詢返回的這個結果起一個新的名稱。

如果需要指定哪些欄位返回,哪些需要過濾,可定義 $project 物件,關聯查詢的欄位過濾可使用 別名.關聯文件中的欄位 進行指定。

const { AuthorModel } = require('./model');
(async () => {
  const res = await AuthorModel.aggregate([
    {
      $match: { authorId1 }
    },
    {
      $lookup: {
        from'books',
        localField'bookIds',
        foreignField'bookId',
        as'bookList',
      }
    },
    {
      $project: {
        '_id'0,
        'authorId'1,
        'name'1,
        'bookList.bookId'1// 指定 books 表的 bookId 欄位返回
        'bookList.name'1
      }
    }
  ]);
  console.log(JSON.stringify(res));
})();

執行以上程式,將得到以下結果:

[
  {
    "authorId":1,
    "name":"Kyle Simpson",
    "bookList":[
      {
        "bookId":26351021,
        "name":"你不知道的JavaScript(上卷)"
      },
      {
        "bookId":26854244,
        "name":"你不知道的JavaScript(中卷)"
      },
      {
        "bookId":27620408,
        "name":"你不知道的JavaScript(下卷)"
      }
    ]
  }
]

關於 $lookup 更多操作參考 MongoDB 官方文件 #lookup-aggregation[2]

Mongoose Virtual 和 populate 實現

Mongoose 的 populate 方法預設情況下是指向的要關聯的集合的 _id 欄位,並且在 populate 方法裡無法更改的,但是在 Mongoose 4.5.0 之後增加了虛擬值填充[3],以便實現文件中更復雜的一些關係。

在我們本節示例中 Authors 集合會關聯 Books 集合,那麼我們就需要在 Authors 集合中定義 virtual, 下面的一些引數和 $lookup 是一樣的,個別引數做下介紹:

  • ref: 表示的要 Join 的集合的名稱,同 $lookup.from
  • justOne: 預設為 false 返回多條資料,如果設定為 true 就只會返回一條資料
AuthorSchema.virtual('bookList', {
  ref'Books',
  localField'bookIds',
  foreignField'bookId',
  justOnefalse,
});

之前在這樣設定之後,發現沒有效果,這裡還要注意一點: 虛擬值預設不會被 toJSON() 或 toObject 輸出。

如果你需要填充的虛擬值的顯示是在 JSON 序列化中輸出,就需要設定 toJSON 屬性,例如 console.log(JSON.stringify(res))。如果是直接顯示的物件,就需要設定 toObject 屬性,例如直接列印 console.log(res)。

可以在建立 Schema 時在第二個引數 options 中設定,也可以使用建立的 Schema 物件的 set 方法設定。

const AuthorSchema = new Schema({
  authorIdNumber,
  nameString,
  bookIds: [{ typeNumberref'Books' }]
}, {
  toJSON: { virtualstrue },
  toObject: { virtualstrue },
});

// 或以下方式
// AuthorSchema.set('toObject', { virtuals: true });
// AuthorSchema.set('toJSON', { virtuals: true });

經過以上設定之後就可以使用 populate 做關聯查詢。

const { AuthorModel } = require('./model');
(async () => {
  const res = await AuthorModel.findOne({ authorId1 })
    .populate({
      path'bookList',
      select'bookId name -_id'
    });
})();

Mongoose 的虛擬值填充,還可以對匹配的文件數量進行計數,使用如下:

// model/author.js
AuthorSchema.virtual('bookListCount', {
  ref'Books',
  localField'bookIds',
  foreignField'bookId',
  counttrue
});

// populateTest.js
const res = await AuthorModel.findOne({ authorId1 }).populate('bookListCount');
console.log(res.bookListCount); // 3


總結

本文主要是介紹了在 Mongoose 關聯查詢時如何關聯一個非 _id 欄位,一種方式是直接使用 MongoDB 原生提供的 Aggregate 聚合管道的 $lookup 階段來實現,這種方式使用起來靈活,可操作的空間更大,例如通過 as 即可對欄位設定別名,還可以使用 $unwind 等關鍵字對資料做二次處理。另外一種是 Mongoose 提供的 populate 方法,這種方式寫起來,程式碼會更簡潔些,這裡需要注意如果關聯的欄位是非 _id 欄位,一定要在 Schema 中設定虛擬值填充,否則 populate 關聯時會失敗

Github 獲取文中程式碼示例 mongoose-populate[4]

參考資料

[1]

mongoing: http://mongoing.com/docs/core/data-modeling-introduction.html#references

[2]

#lookup-aggregation: http://docs.mongodb.com/v4.2/reference/operator/aggregation/lookup/index.html

[3]

虛擬值填充: http://www.mongoosejs.net/docs/populate.html#populate-virtuals

[4]

mongoose-populate: http://github.com/qufei1993/Examples/tree/master/code/database/mongoose-populate

- END -

敬請關注「Nodejs技術棧」微信公眾號,獲取優質文章,如需投稿可在後臺留言與我取得聯絡。

往期精彩回顧
Node.js + Socket.io 實現一對一即時聊天
實現瀏覽器中的最大請求併發數控制
如何為團隊定製自己的 Node.js 框架?(基於 EggJS)
Node.js 在企業中的應用實踐集錦 - 2020 年中彙總
多維度分析 Express、Koa 之間的區別
Nodejs Stream pipe 的使用與實現原理分析
跨域(CORS)產生原因分析與解決方案,這一次徹底搞懂它
如何處理 Node.js 中出現的未捕獲異常?

本文分享自微信公眾號 - Nodejs技術棧(NodejsRoadmap)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。