你真的瞭解mongoose嗎?

引言

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

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

mongoose 涉及到的概念和模塊仍是不少的,大致有下面這些:node

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

模式(schemas)

定義你的 schema

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

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 有幾個可配置的選項,能夠直接傳遞給構造函數或設置:mongodb

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#optionsapi

這裏我主要說一下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 將字段值乘以指定數量
    &dollar;unset 刪除指定字段,數組中的值刪後改成 null。

    數組字段相關操做符

    符號 描述
    &dollar; 充當佔位符,用來表示匹配查詢條件的數組字段中的第一個元素 {operator:{ "arrayField.$" : value }}
    &dollar;addToSet 向數組字段中添加以前不存在的元素 { $addToSet: {arrayField: value, ... }},value 是數組時可與 $each 組合使用。
    &dollar;push 向數組字段的末尾添加元素 { $push: { arrayField: value, ... } },value 是數組時可與 $each 等修飾符組合使用
    &dollar;pop 移除數組字段中的第一個或最後一個元素 { $pop: {arrayField: -1(first) / 1(last), ... } }
    &dollar;pull 移除數組字段中與查詢條件匹配的全部元素 { $pull: {arrayField: value / condition, ... } }
    &dollar;pullAll 從數組中刪除全部匹配的值 { $pullAll: { arrayField: [value1, value2 ... ], ... } }

    修飾符

    符號 描述
    &dollar;each 修飾 $push$addToSet 操做符,以便爲數組字段添加多個元素。
    &dollar;position 修飾 $push 操做符以指定要添加的元素在數組中的位置。
    &dollar;slice 修飾 $push 操做符以限制更新後的數組的大小。
    &dollar;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/Jack-cool/rest_node_api)中對於mongoose的使用作的簡單的總結。但願能給你帶來幫助!

同時你能夠關注個人同名公衆號【前端森林】,這裏我會按期發一些大前端相關的前沿文章和平常開發過程當中的實戰總結。

相關文章
相關標籤/搜索