引言
繼上篇文章「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);
這裡的__v
是versionKey
。該 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
。
這裡我主要說一下versionKey
和timestamps
:
versionKey
(上文有提到) 是 Mongoose 在檔案建立時自動設定的。 這個值包含檔案的內部修訂號。 versionKey 是一個字串,代表版本號的屬性名, 預設值為__v
- 如果設定了
timestamps
選項, mongoose 會在你的 schema 自動新增createdAt
和updatedAt
欄位, 其型別為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()
定義自定義 getterset
: 函式,使用Object.defineProperty()
定義自定義 setteralias
: 字串,只對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
: Datemax
: 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.user
和auth.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 裡面的三個概念:schema
、model
和entity
:
schema
: 一種以檔案形式儲存的資料庫模型骨架,不具備資料庫的操作能力model
: 由 schema 釋出生成的模型,具有抽象屬性和行為的資料庫操作對entity
: 由 Model 建立的實體,他的操作也會影響資料庫
Schema、Model、Entity 的關係請牢記: Schema生成Model,Model創造Entity
,Model 和 Entity 都可對資料庫操作造成影響,但 Model 比 Entity 更具操作性。
查詢
對於 Mongoosecha 的查詢文件很容易,它支援豐富的查詢 MongoDB 語法。包括find
、findById
、findOne
等。
find()
第一個參數列示查詢條件,第二個引數用於控制返回的欄位,第三個引數用於配置查詢引數,第四個引數是回撥函式,回撥函式的形式為function(err,docs){}
Model.find(conditions, [projection], [options], [callback])
下面讓我們依次看下 find()的各個引數在實際場景中的應用:
conditions
- 查詢全部
Model.find({})
- 精確查詢
Model.find({name:'森林'})
- 使用操作符
對比相關操作符
Model.find({ age: { $in: [18, 24]} })
返回 age
欄位等於 18
或者 24
的所有 document。
邏輯相關操作符
// 返回 age 欄位大於 24 或者 age 欄位不存在的文件
Model.find( { age: { $not: { $lte: 24 }}})
欄位相關操作符
陣列欄位的查詢
// 使用 $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});
sort
和 limit
同時使用時,呼叫的順序並不重要,返回的資料都是先排序後限制數量。
// 效果一樣
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 })
。
看一下官方對於findOne
與findById
的對比:
不同之處在於處理 id 為undefined
時的情況。findOne({ _id: undefined })
相當於findOne({})
,返回任意一條資料。而findById(undefined)
相當於findOne({ _id: null })
,返回null
。
查詢結果:
- 返回資料的格式是
{}
物件形式。 - id 為
undefined
或null
,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 的原生結果。
- lean: true 返回普通的 js 物件,而不是
callback
- 沒找到資料返回
null
- 更新成功返回更新前的該條資料(
{}
形式) options
的{new:true}
,更新成功返回更新後的該條資料({}
形式)- 沒有查詢條件,即
filter
為空,則更新第一條資料
- 沒找到資料返回
findByIdAndUpdate()
Model.findByIdAndUpdate(id, update, options, callback)
Model.findByIdAndUpdate(id, update)
相當於 Model.findOneAndUpdate({ _id: id }, update)
。
result 查詢結果:
- 返回資料的格式是
{}
物件形式。 - id 為
undefined
或null
,result 返回null
。 - 沒符合查詢條件的資料,result 返回
null
。
update()
Model.update(filter, update, options, callback)
options
- multi: 預設
false
,只更新第一條資料;為true
時,符合查詢條件的多條文件都會更新。 - overwrite:預設為
false
,即update
引數如果沒有操作符或操作符不是 update 操作符,將會預設新增$set
;如果為true
,則不新增$set
,視為覆蓋原有文件。
- multi: 預設
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
。
ObjectId
、Number
、String
以及 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
的使用做的簡單的總結。希望能給你帶來幫助!
同時你可以關注我的同名公眾號【前端森林】,這裡我會定期發一些大前端相關的前沿文章和日常開發過程中的實戰總結。