你真的瞭解mongoose嗎?

前端森林發表於2020-11-10

引言

繼上篇文章「Koa2+MongoDB+JWT實戰--Restful API最佳實踐」後,收到許多小夥伴的反饋,表示自己對於mongoose不怎麼了解,上手感覺有些難度,看官方文件又基本都是英文(寶寶心裡苦,但寶寶不說)。

為了讓各位小夥伴快速上手,加深對於 mongoose 的瞭解,我特地結合之前的專案整理了一下關於 mongoose 的一些基礎知識,這些對於實戰都是很有用的。相信看了這篇文章,一定會對你快速上手,瞭解使用 mongoose 有不小的幫助。

mongoose 涉及到的概念和模組還是很多的,大體有下面這些:

本篇文章並不會逐個去展開詳細講解,主要是講述在實戰中比較重要的幾個模組:模式(schemas)模式型別(SchemaTypes)連線(Connections)模型(Models)聯表(Populate)

模式(schemas)

定義你的 schema

Mongoose的一切都始於一個Schema。每個 schema 對映到 MongoDB 的集合(collection)和定義該集合(collection)中的文件的形式。

const mongoose = require("mongoose");

const { Schema, model } = mongoose;

const userSchema = new Schema(
  {
    __v: { type: Number, select: false },
    name: { type: String, required: true },
    password: { type: String, required: true, select: false },
    avatar_url: { type: String },
    gender: {
      type: String,
      enum: ["male", "female"],
      default: "male",
      required: true
    },
    headline: { type: String },
  },
  { timestamps: true }
);

module.exports = model("User", userSchema);
這裡的__vversionKey。該 versionKey 是每個文件首次建立時,由 mongoose 建立的一個屬性。包含了文件的內部修訂版。此文件屬性是可配置的。預設值為__v。如果不需要該版本號,在 schema 中新增{ versionKey: false}即可。

建立模型

使用我們的 schema 定義,我們需要將我們的userSchema轉成我們可以用的模型。也就是mongoose.model(modelName, schema) 。也就是上面程式碼中的:

module.exports = model("User", userSchema);

選項(options)

Schemas 有幾個可配置的選項,可以直接傳遞給建構函式或設定:

new Schema({..}, options);

// or

var schema = new Schema({..});
schema.set(option, value);

可用選項:

  • autoIndex
  • bufferCommands
  • capped
  • collection
  • id
  • _id
  • minimize
  • read
  • shardKey
  • strict
  • toJSON
  • toObject
  • typeKey
  • validateBeforeSave
  • versionKey
  • skipVersioning
  • timestamps

這裡我只是列舉了常用的配置項,完整的配置項可檢視官方文件https://mongoosejs.com/docs/guide.html#options

這裡我主要說一下versionKeytimestamps:

  • versionKey(上文有提到) 是 Mongoose 在檔案建立時自動設定的。 這個值包含檔案的內部修訂號。 versionKey 是一個字串,代表版本號的屬性名, 預設值為 __v
  • 如果設定了 timestamps 選項, mongoose 會在你的 schema 自動新增 createdAtupdatedAt 欄位, 其型別為 Date

到這裡,已經基本介紹完了Schema,接下來看一下SchemaTypes

模式型別(SchemaTypes)

SchemaTypes為查詢和其他處理路徑預設值,驗證,getter,setter,欄位選擇預設值,以及字串和數字的特殊字元。 在 mongoose 中有效的 SchemaTypes 有:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
  • Decimal128
  • Map

看一個簡單的示例:

const answerSchema = new Schema(
  {
    __v: { type: Number, select: false },
    content: { type: String, required: true },
    answerer: {
      type: Schema.Types.ObjectId,
      ref: "User",
      required: true,
      select: false
    },
    questionId: { type: String, required: true },
    voteCount: { type: Number, required: true, default: 0 }
  },
  { timestamps: true }
);

所有的 Schema 型別

  • required: 布林值或函式,如果為 true,則為此屬性新增必須的驗證。
  • default: 任意型別或函式,為路徑設定一個預設的值。如果值是一個函式,則函式的返回值用作預設值。
  • select: 布林值 指定 query 的預設 projections
  • validate: 函式,對屬性新增驗證函式。
  • get: 函式,使用 Object.defineProperty() 定義自定義 getter
  • set: 函式,使用 Object.defineProperty() 定義自定義 setter
  • alias: 字串,只對mongoose>=4.10.0有效。定義一個具有給定名稱的虛擬屬性,該名稱可以獲取/設定這個路徑

索引

你可以用 schema 型別選項宣告 MongoDB 的索引。

  • index: 布林值,是否在屬性中定義一個索引。
  • unique: 布林值,是否在屬性中定義一個唯一索引。
  • sparse: 布林值,是否在屬性中定義一個稀疏索引。
var schema2 = new Schema({
  test: {
    type: String,
    index: true,
    unique: true // 如果指定`unique`為true,則為唯一索引
  }
});

字串

  • lowercase: 布林值,是否在儲存前對此值呼叫toLowerCase()
  • uppercase: 布林值,是否在儲存前對此值呼叫toUpperCase()
  • trim: 布林值,是否在儲存前對此值呼叫trim()
  • match: 正則,建立一個驗證器,驗證值是否匹配給定的正規表示式
  • enum: 陣列,建立一個驗證器,驗證值是否是給定陣列中的元素

數字

  • min: 數字,建立一個驗證器,驗證值是否大於等於給定的最小值
  • max: 數字,建立一個驗證器,驗證值是否小於等於給定的最大的值

日期

  • min: Date
  • max: Date

現在已經介紹完Schematype,接下來讓我們看一下Connections

連線(Connections)

我們可以通過利用mongoose.connect()方法連線 MongoDB 。

mongoose.connect('mongodb://localhost:27017/myapp');

這是連線執行在本地myapp資料庫最小的值(27017)。如果連線失敗,嘗試用127.0.0.1代替localhost

當然,你可在 uri 中指定更多的引數:

mongoose.connect('mongodb://username:password@host:port/database?options...');

操作快取

意思就是我們不必等待連線建立成功就可以使用 models,mongoose 會先快取 model 操作

let TestModel = mongoose.model('Test', new Schema({ name: String }));
// 連線成功前操作會被掛起
TestModel.findOne(function(error, result) { /* ... */ });

setTimeout(function() {
  mongoose.connect('mongodb://localhost/myapp');
}, 60000);

如果要禁用快取,可修改bufferCommands配置,也可以全域性禁用 bufferCommands

mongoose.set('bufferCommands', false);

選項

connect 方法也接收一個 options 物件:

mongoose.connect(uri, options);

這裡我列舉幾個在日常使用中比較重要的選項,完整的連線選項看這裡

  • bufferCommands:這是 mongoose 中一個特殊的選項(不傳遞給 MongoDB 驅動),它可以禁用 mongoose 的緩衝機制
  • user/pass:身份驗證的使用者名稱和密碼。這是 mongoose 中特殊的選項,它們可以等同於 MongoDB 驅動中的auth.userauth.password選項。
  • dbName:指定連線哪個資料庫,並覆蓋連線字串中任意的資料庫。
  • useNewUrlParser:底層 MongoDB 已經廢棄當前連線字串解析器。因為這是一個重大的改變,新增了 useNewUrlParser 標記如果在使用者遇到 bug 時,允許使用者在新的解析器中返回舊的解析器。
  • poolSize:MongoDB 驅動將為這個連線保持的最大 socket 數量。預設情況下,poolSize 是 5。
  • useUnifiedTopology:預設情況下為false。設定為 true 表示選擇使用 MongoDB 驅動程式的新連線管理引擎。您應該將此選項設定為 true,除非極少數情況會阻止您保持穩定的連線。

示例:

const options = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  autoIndex: false, // 不建立索引
  reconnectTries: Number.MAX_VALUE, // 總是嘗試重新連線
  reconnectInterval: 500, // 每500ms重新連線一次
  poolSize: 10, // 維護最多10個socket連線
  // 如果沒有連線立即返回錯誤,而不是等待重新連線
  bufferMaxEntries: 0,
  connectTimeoutMS: 10000, // 10s後放棄重新連線
  socketTimeoutMS: 45000, // 在45s不活躍後關閉sockets
  family: 4 // 用IPv4, 跳過IPv6
};
mongoose.connect(uri, options);

回撥

connect()函式也接收一個回撥引數,其返回一個 promise。

mongoose.connect(uri, options, function(error) {
  // 檢查錯誤,初始化連線。回撥沒有第二個引數。
});

// 或者用promise
mongoose.connect(uri, options).then(
  () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },
  err => { /** handle initial connection error */ }
);

說完Connections,下面讓我們來看一個重點Models

模型(Models)

Models 是從 Schema 編譯來的建構函式。 它們的例項就代表著可以從資料庫儲存和讀取的 documents。 從資料庫建立和讀取 document 的所有操作都是通過 model 進行的。

const mongoose = require("mongoose");

const { Schema, model } = mongoose;

const answerSchema = new Schema(
  {
    __v: { type: Number, select: false },
    content: { type: String, required: true },
  },
  { timestamps: true }
);

module.exports = model("Answer", answerSchema);

定義好 model 之後,就可以進行一些增刪改查操作了

建立

如果是Entity,使用save方法;如果是Model,使用create方法或insertMany方法。

// save([options], [options.safe], [options.validateBeforeSave], [fn])
let Person = mongoose.model("User", userSchema);
let person1 = new Person({ name: '森林' });
person1.save()

// 使用save()方法,需要先例項化為文件,再使用save()方法儲存文件。而create()方法,則直接在模型Model上操作,並且可以同時新增多個文件
// Model.create(doc(s), [callback])
Person.create({ name: '森林' }, callback)

// Model.insertMany(doc(s), [options], [callback])
Person.insertMany([{ name: '森林' }, { name: '之晨' }], function(err, docs) {

})

說到這裡,我們先要補充說明一下 mongoose 裡面的三個概念:schemamodelentity:

  • schema: 一種以檔案形式儲存的資料庫模型骨架,不具備資料庫的操作能力
  • model: 由 schema 釋出生成的模型,具有抽象屬性和行為的資料庫操作對
  • entity: 由 Model 建立的實體,他的操作也會影響資料庫
Schema、Model、Entity 的關係請牢記: Schema生成Model,Model創造Entity,Model 和 Entity 都可對資料庫操作造成影響,但 Model 比 Entity 更具操作性。

查詢

對於 Mongoosecha 的查詢文件很容易,它支援豐富的查詢 MongoDB 語法。包括findfindByIdfindOne等。

find()

第一個參數列示查詢條件,第二個引數用於控制返回的欄位,第三個引數用於配置查詢引數,第四個引數是回撥函式,回撥函式的形式為function(err,docs){}

Model.find(conditions, [projection], [options], [callback])

下面讓我們依次看下 find()的各個引數在實際場景中的應用:

  • conditions

    • 查詢全部
    Model.find({})
    • 精確查詢
    Model.find({name:'森林'})
    • 使用操作符

對比相關操作符

compareOp.png

Model.find({ age: { $in: [18, 24]} })

返回 age 欄位等於 18 或者 24 的所有 document。

邏輯相關操作符
logicOp.png

// 返回 age 欄位大於 24 或者 age 欄位不存在的文件
Model.find( { age: { $not: { $lte: 24 }}})

欄位相關操作符
fieldOp.png

陣列欄位的查詢
arrFieldOp.png

// 使用 $all 查詢同時存在 18 和 20 的 document
Model.find({ age: { $all: [ 18, 20 ] } });
  • projection

    指定要包含或排除哪些 document 欄位(也稱為查詢“投影”),必須同時指定包含或同時指定排除,不能混合指定,_id除外。

    在 mongoose 中有兩種指定方式,字串指定物件形式指定

    字串指定時在排除的欄位前加 - 號,只寫欄位名的是包含。

    Model.find({},'age');
    Model.find({},'-name');

    物件形式指定時,1 是包含,0 是排除。

    Model.find({}, { age: 1 });
    Model.find({}, { name: 0 });
  • options

    // 三種方式實現
    Model.find(filter,null,options)
    Model.find(filter).setOptions(options)
    Model.find(filter).<option>(xxx)

    options 選項見官方文件 Query.prototype.setOptions()

    這裡我們只列舉常用的:

    • sort: 按照排序規則根據所給的欄位進行排序,值可以是 asc, desc, ascending, descending, 1, 和 -1。
    • limit: 指定返回結果的最大數量
    • skip: 指定要跳過的文件數量
    • lean: 返回普通的 js 物件,而不是 Mongoose Documents。建議不需要 mongoose 特殊處理就返給前端的資料都最好使用該方法轉成普通 js 物件。
    // sort 兩種方式指定排序
    Model.find().sort('age -name'); // 字串有 - 代表 descending 降序
    Model.find().sort({age:'asc', name:-1});

sortlimit 同時使用時,呼叫的順序並不重要,返回的資料都是先排序後限制數量。

// 效果一樣
Model.find().limit(2).sort('age');
Model.find().sort('age').limit(2);
  • callback

    Mongoose 中所有傳入 callback 的查詢,其格式都是 callback(error, result) 這種形式。如果出錯,則 error 是出錯資訊,result 是 null;如果查詢成功,則 error 是 null, result 是查詢結果,查詢結果的結構形式是根據查詢方法的不同而有不同形式的。

    find() 方法的查詢結果是陣列,即使沒查詢到內容,也會返回 [] 空陣列。

findById

Model.findById(id,[projection],[options],[callback])

Model.findById(id) 相當於 Model.findOne({ _id: id })

看一下官方對於findOnefindById的對比:

不同之處在於處理 id 為 undefined 時的情況。findOne({ _id: undefined }) 相當於 findOne({}),返回任意一條資料。而 findById(undefined) 相當於 findOne({ _id: null }),返回 null

查詢結果:

  • 返回資料的格式是 {} 物件形式。
  • id 為 undefinednull,result 返回 null
  • 沒符合查詢條件的資料,result 返回 null

findOne

該方法返回查詢到的所有例項的第一個

Model.findOne(conditions, [projection], [options], [callback])

如果查詢條件是 _id,建議使用 findById()

查詢結果:

  • 返回資料的格式是 {} 物件形式。
  • 有多個資料滿足查詢條件的,只返回第一條。
  • 查詢條件 conditions 為 {}、 null 或 undefined,將任意返回一條資料。
  • 沒有符合查詢條件的資料,result 返回 null。

更新

每個模型都有自己的更新方法,用於修改資料庫中的文件,不將它們返回到您的應用程式。常用的有findOneAndUpdate()findByIdAndUpdate()update()updateMany()等。

findOneAndUpdate()

Model.findOneAndUpdate(filter, update, [options], [callback])
  • filter

    查詢語句,和find()一樣。

    filter 為{},則只更新第一條資料。

  • update

    {operator: { field: value, ... }, ... }
    必須使用 update 操作符。如果沒有操作符或操作符不是 update 操作符,統一被視為 $set 操作(mongoose 特有)

    欄位相關操作符

    符號描述
    $set設定欄位值
    $currentDate設定欄位值為當前時間,可以是 Date 或時間戳格式。
    $min只有當指定值小於當前欄位值時更新
    $max只有當指定值大於當前欄位值時更新
    $inc將欄位值增加指定數量指定數量可以是負數,代表減少。
    $mul將欄位值乘以指定數量
    \$unset刪除指定欄位,陣列中的值刪後改為 null。

    陣列欄位相關操作符

    符號描述
    \$充當佔位符,用來表示匹配查詢條件的陣列欄位中的第一個元素 {operator:{ "arrayField.$" : value }}
    \$addToSet向陣列欄位中新增之前不存在的元素 { $addToSet: {arrayField: value, ... }},value 是陣列時可與 $each 組合使用。
    \$push向陣列欄位的末尾新增元素 { $push: { arrayField: value, ... } },value 是陣列時可與 $each 等修飾符組合使用
    \$pop移除陣列欄位中的第一個或最後一個元素 { $pop: {arrayField: -1(first) / 1(last), ... } }
    \$pull移除陣列欄位中與查詢條件匹配的所有元素 { $pull: {arrayField: value / condition, ... } }
    \$pullAll從陣列中刪除所有匹配的值 { $pullAll: { arrayField: [value1, value2 ... ], ... } }

    修飾符

    符號描述
    \$each修飾 $push$addToSet 操作符,以便為陣列欄位新增多個元素。
    \$position修飾 $push 操作符以指定要新增的元素在陣列中的位置。
    \$slice修飾 $push 操作符以限制更新後的陣列的大小。
    \$sort修飾 $push 操作符來重新排序陣列欄位中的元素。

    修飾符執行的順序(與定義的順序無關):

    • 在指定的位置新增元素以更新陣列欄位
    • 按照指定的規則排序
    • 限制陣列大小
    • 儲存陣列
  • options

    • lean: true 返回普通的 js 物件,而不是 Mongoose Documents
    • new: 布林值,true 返回更新後的資料,false (預設)返回更新前的資料。
    • fields/select:指定返回的欄位。
    • sort:如果查詢條件找到多個文件,則設定排序順序以選擇要更新哪個文件。
    • maxTimeMS:為查詢設定時間限制。
    • upsert:布林值,如果物件不存在,則建立它。預設值為 false
    • omitUndefined:布林值,如果為 true,則在更新之前刪除值為 undefined 的屬性。
    • rawResult:如果為 true,則返回來自 MongoDB 的原生結果。
  • callback

    • 沒找到資料返回 null
    • 更新成功返回更新前的該條資料( {} 形式)
    • options{new:true},更新成功返回更新後的該條資料( {} 形式)
    • 沒有查詢條件,即 filter 為空,則更新第一條資料

findByIdAndUpdate()

Model.findByIdAndUpdate(id, update, options, callback)

Model.findByIdAndUpdate(id, update) 相當於 Model.findOneAndUpdate({ _id: id }, update)

result 查詢結果:

  • 返回資料的格式是 {} 物件形式。
  • id 為 undefinednull,result 返回 null
  • 沒符合查詢條件的資料,result 返回 null

update()

Model.update(filter, update, options, callback)
  • options

    • multi: 預設 false,只更新第一條資料;為 true 時,符合查詢條件的多條文件都會更新。
    • overwrite:預設為 false,即 update 引數如果沒有操作符或操作符不是 update 操作符,將會預設新增 $set;如果為 true,則不新增 $set,視為覆蓋原有文件。

updateMany()

Model.updateMany(filter, update, options, callback)

更新符合查詢條件的所有文件,相當於 Model.update(filter, update, { multi: true }, callback)

刪除

刪除常用的有findOneAndDelete()findByIdAndDelete()deleteMany()findByIdAndRemove()等。

findOneAndDelete()

Model.findOneAndDelete(filter, options, callback)
  • filter
    查詢語句和 find() 一樣
  • options

    • sort:如果查詢條件找到多個文件,則設定排序順序以選擇要刪除哪個文件。
    • select/projection:指定返回的欄位。
    • rawResult:如果為 true,則返回來自 MongoDB 的原生結果。
  • callback

    • 沒有符合 filter 的資料時,返回 null
    • filter 為空或 {} 時,刪除第一條資料。
    • 刪除成功返回 {} 形式的原資料。

findByIdAndDelete()

Model.findByIdAndDelete(id, options, callback)

Model.findByIdAndDelete(id) 相當於 Model.findOneAndDelete({ _id: id })

  • callback

    • 沒有符合 id 的資料時,返回 null
    • id 為空或 undefined 時,返回 null
    • 刪除成功返回 {} 形式的原資料。

deleteMany()

Model.deleteMany(filter, options, callback)
  • filter
    刪除所有符合 filter 條件的文件。

deleteOne()

Model.deleteOne(filter, options, callback)
  • filter
    刪除符合 filter 條件的第一條文件。

findOneAndRemove()

Model.findOneAndRemove(filter, options, callback)

用法與 findOneAndDelete() 一樣,一個小小的區別是 findOneAndRemove() 會呼叫 MongoDB 原生的 findAndModify() 命令,而不是 findOneAndDelete() 命令。

建議使用 findOneAndDelete() 方法。

findByIdAndRemove()

Model.findByIdAndRemove(id, options, callback)

Model.findByIdAndRemove(id) 相當於 Model.findOneAndRemove({ _id: id })

remove()

Model.remove(filter, options, callback)

從集合中刪除所有匹配 filter 條件的文件。要刪除第一個匹配條件的文件,可將 single 選項設定為 true

看完Models,最後讓我們來看下在實戰中比較有用的Populate

聯表(Populate)

Mongoose 的 populate() 可以連表查詢,即在另外的集合中引用其文件。

Populate() 可以自動替換 document 中的指定欄位,替換內容從其他 collection 中獲取。

refs

建立 Model 的時候,可給該 Model 中關聯儲存其它集合 _id 的欄位設定 ref 選項。ref 選項告訴 Mongoose 在使用 populate() 填充的時候使用哪個 Model

const mongoose = require("mongoose");

const { Schema, model } = mongoose;

const answerSchema = new Schema(
  {
    __v: { type: Number, select: false },
    content: { type: String, required: true },
    answerer: {
      type: Schema.Types.ObjectId,
      ref: "User",
      required: true,
      select: false
    },
    questionId: { type: String, required: true },
    voteCount: { type: Number, required: true, default: 0 }
  },
  { timestamps: true }
);

module.exports = model("Answer", answerSchema);

上例中 Answer model 的 answerer 欄位設為 ObjectId 陣列。 ref 選項告訴 Mongoose 在填充的時候使用 User model。所有儲存在 answerer 中的 _id 都必須是 User model 中 document_id

ObjectIdNumberString 以及 Buffer 都可以作為 refs 使用。 但是最好還是使用 ObjectId

在建立文件時,儲存 refs 欄位與儲存普通屬性一樣,把 _id 的值賦給它就好了。

const Answer = require("../models/answers");

async create(ctx) {
  ctx.verifyParams({
    content: { type: "string", required: true }
  });
  const answerer = ctx.state.user._id;
  const { questionId } = ctx.params;
  const answer = await new Answer({
    ...ctx.request.body,
    answerer,
    questionId
  }).save();
  ctx.body = answer;
}

populate(path,select)

填充document

const Answer = require("../models/answers");

const answer = await Answer.findById(ctx.params.id)
      .select(selectFields)
      .populate("answerer");

被填充的 answerer 欄位已經不是原來的 _id,而是被指定的 document 代替。這個 document 由另一條 query 從資料庫返回。

返回欄位選擇

如果只需要填充 document 中一部分欄位,可給 populate() 傳入第二個引數,引數形式即 返回欄位字串,同 Query.prototype.select()

const answer = await Answer.findById(ctx.params.id)
      .select(selectFields)
      .populate("answerer", "name -_id");

populate 多個欄位

const populateStr =
      fields &&
      fields
        .split(";")
        .filter(f => f)
        .map(f => {
          if (f === "employments") {
            return "employments.company employments.job";
          }
          if (f === "educations") {
            return "educations.school educations.major";
          }
          return f;
        })
        .join(" ");
const user = await User.findById(ctx.params.id)
      .select(selectFields)
      .populate(populateStr);

最後

到這裡本篇文章也就結束了,這裡主要是結合我平時的專案(https://github.com/Cosen95/rest_node_api)中對於mongoose的使用做的簡單的總結。希望能給你帶來幫助!

同時你可以關注我的同名公眾號【前端森林】,這裡我會定期發一些大前端相關的前沿文章和日常開發過程中的實戰總結。

image

相關文章