中介軟體
中介軟體(也稱為 pre 和 post hooks)是在執行非同步函式時將控制權傳遞給的函式。中介軟體是在綱要層級指定,對於編寫 外掛程式很有用。
- 中介軟體類型
- Pre
- Pre Hook 中的錯誤
- Post
- 非同步 Post Hook
- 在編譯模型之前定義中介軟體
- Save/Validate Hook
- 在 Middleware 中存取參數
- 命名衝突
- 關於 findAndUpdate() 和查詢中介軟體的注意事項
- 錯誤處理中介軟體
- 彙總 Hook
- 同步 Hook
中介軟體類型
Mongoose 有 4 種類型的中介軟體:文件中間件、模型中間件、聚合中間件和查詢中間件。
以下文件函數支援文件中間件。在 Mongoose 中,文件是 Model
類別的實例。在文件中間件函數中,this
指的是文件。若要存取模型,請使用 this.constructor
。
以下 Query 函式支援查詢中介軟體。當您在 Query 物件上呼叫 exec()
或 then()
,或在 Query 物件上呼叫 await
時,會執行查詢中介軟體。在查詢中介軟體函數中,this
指的是查詢。
- count
- countDocuments
- deleteMany
- deleteOne
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
- replaceOne
- updateOne
- updateMany
- validate
彙總中介軟體用於 MyModel.aggregate()
。當您在彙總物件上呼叫 exec()
時,會執行彙總中介軟體。在彙總中介軟體中,this
指的是彙總物件。
以下模型函數支援模型中介軟體。不要混淆模型中介軟體和文件中間件:模型中介軟體會掛鉤到 Model
類別上的靜態函數,而文件中間件則會掛鉤到 Model
類別上的方法。在模型中間件函數中,this
指的是模型。
以下是可以傳遞給 pre()
的可能字串
- aggregate
- bulkWrite
- count
- countDocuments
- createCollection
- deleteOne
- deleteMany
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
- init
- insertMany
- replaceOne
- save
- update
- updateOne
- updateMany
- validate
所有中介軟體類型都支援 pre 和 post hook。以下將更詳細地說明 pre 和 post hook 的運作方式。
注意: Mongoose 預設會在 Query.prototype.updateOne()
上註冊 updateOne
中介軟體。這表示 doc.updateOne()
和 Model.updateOne()
都會觸發 updateOne
hook,但 this
指的是查詢,而不是文件。若要將 updateOne
中介軟體註冊為文件中間件,請使用 schema.pre('updateOne', { document: true, query: false })
。
注意: 與 updateOne
類似,Mongoose 預設會在 Query.prototype.deleteOne
上註冊 deleteOne
中介軟體。這表示 Model.deleteOne()
會觸發 deleteOne
hook,而 this
會指的是查詢。然而,由於舊有原因,doc.deleteOne()
並不會觸發 deleteOne
查詢中介軟體。若要將 deleteOne
中介軟體註冊為文件中間件,請使用 schema.pre('deleteOne', { document: true, query: false })
。
注意: create()
函數會觸發 save()
hook。
注意: 查詢中介軟體不會在子文件上執行。
const childSchema = new mongoose.Schema({
name: String
});
const mainSchema = new mongoose.Schema({
child: [childSchema]
});
mainSchema.pre('findOneAndUpdate', function() {
console.log('Middleware on parent document'); // Will be executed
});
childSchema.pre('findOneAndUpdate', function() {
console.log('Middleware on subdocument'); // Will not be executed
});
Pre
當每個中介軟體呼叫 next
時,會依序執行 Pre 中介軟體函數。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
在 mongoose 5.x 中,您可以使用傳回 promise 的函數,而不是手動呼叫 next()
。特別是,您可以使用 async/await
。
schema.pre('save', function() {
return doStuff().
then(() => doMoreStuff());
});
// Or, using async functions
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
如果您使用 next()
,則 next()
呼叫並不會停止執行中介軟體函數中其餘的程式碼。當您呼叫 next()
時,請使用提前 return
模式,以防止執行中介軟體函數的其餘部分。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
if (foo()) {
console.log('calling next!');
// `return next();` will make sure the rest of this function doesn't run
/* return */ next();
}
// Unless you comment out the `return` above, 'after next' will print
console.log('after next');
});
使用案例
中介軟體對於模型邏輯原子化非常有用。以下是一些其他想法
- 複雜的驗證
- 移除相依的文件(移除使用者會移除其所有部落格文章)
- 非同步預設值
- 特定動作觸發的非同步工作
Pre Hook 中的錯誤
如果任何 pre hook 發生錯誤,mongoose 將不會執行後續的中介軟體或已連結的函數。Mongoose 會改為將錯誤傳遞給回呼和/或拒絕傳回的 promise。有幾種方法可以在中介軟體中報告錯誤
schema.pre('save', function(next) {
const err = new Error('something went wrong');
// If you call `next()` with an argument, that argument is assumed to be
// an error.
next(err);
});
schema.pre('save', function() {
// You can also return a promise that rejects
return new Promise((resolve, reject) => {
reject(new Error('something went wrong'));
});
});
schema.pre('save', function() {
// You can also throw a synchronous error
throw new Error('something went wrong');
});
schema.pre('save', async function() {
await Promise.resolve();
// You can also throw an error in an `async` function
throw new Error('something went wrong');
});
// later...
// Changes will not be persisted to MongoDB because a pre hook errored out
myDoc.save(function(err) {
console.log(err.message); // something went wrong
});
多次呼叫 next()
無作用。如果您呼叫 next()
並傳入錯誤 err1
,然後拋出錯誤 err2
,則 mongoose 會報告 err1
。
Post 中介軟體
post 中介軟體是在已連結的方法及其所有 pre
中介軟體完成之後執行。
schema.post('init', function(doc) {
console.log('%s has been initialized from the db', doc._id);
});
schema.post('validate', function(doc) {
console.log('%s has been validated (but not saved yet)', doc._id);
});
schema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
schema.post('deleteOne', function(doc) {
console.log('%s has been deleted', doc._id);
});
非同步 Post Hook
如果您的 post hook 函數採用至少 2 個參數,則 mongoose 會假設第二個參數是您將呼叫以觸發序列中下一個中介軟體的 next()
函數。
// Takes 2 parameters: this is an asynchronous post hook
schema.post('save', function(doc, next) {
setTimeout(function() {
console.log('post1');
// Kick off the second post hook
next();
}, 10);
});
// Will not execute until the first middleware calls `next()`
schema.post('save', function(doc, next) {
console.log('post2');
next();
});
您也可以將非同步函數傳遞給 post()
。如果您傳遞的非同步函數採用至少 2 個參數,您仍然有責任呼叫 next()
。但是,您也可以傳入採用少於 2 個參數的非同步函數,而且 Mongoose 會等待 promise 解決。
schema.post('save', async function(doc) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('post1');
// If less than 2 parameters, no need to call `next()`
});
schema.post('save', async function(doc, next) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('post1');
// If there's a `next` parameter, you need to call `next()`.
next();
});
在編譯模型之前定義中介軟體
在 編譯模型之後呼叫 pre()
或 post()
一般在 Mongoose 中不起作用。例如,以下 pre('save')
中介軟體不會觸發。
const schema = new mongoose.Schema({ name: String });
// Compile a model from the schema
const User = mongoose.model('User', schema);
// Mongoose will **not** call the middleware function, because
// this middleware was defined after the model was compiled
schema.pre('save', () => console.log('Hello from pre save'));
const user = new User({ name: 'test' });
user.save();
這表示您必須在呼叫 mongoose.model()
之前加入所有中介軟體和外掛程式。以下指令碼將列印出「來自 pre save 的 Hello」
const schema = new mongoose.Schema({ name: String });
// Mongoose will call this middleware function, because this script adds
// the middleware to the schema before compiling the model.
schema.pre('save', () => console.log('Hello from pre save'));
// Compile a model from the schema
const User = mongoose.model('User', schema);
const user = new User({ name: 'test' });
user.save();
因此,請務必小心,不要從您定義綱要的同一個檔案匯出 Mongoose 模型。如果您選擇使用此模式,您必須在對模型檔案呼叫 require()
之前定義全域外掛程式。
const schema = new mongoose.Schema({ name: String });
// Once you `require()` this file, you can no longer add any middleware
// to this schema.
module.exports = mongoose.model('User', schema);
Save/Validate Hook
save()
函數會觸發 validate()
hook,因為 mongoose 有一個內建的 pre('save')
hook 會呼叫 validate()
。這表示所有 pre('validate')
和 post('validate')
hook 都會在任何 pre('save')
hook之前被呼叫。
schema.pre('validate', function() {
console.log('this gets printed first');
});
schema.post('validate', function() {
console.log('this gets printed second');
});
schema.pre('save', function() {
console.log('this gets printed third');
});
schema.post('save', function() {
console.log('this gets printed fourth');
});
在 Middleware 中存取參數
Mongoose 提供兩種方式來取得有關觸發中介軟體之函數呼叫的資訊。對於查詢中介軟體,我們建議使用 this
,它會是 Mongoose Query 實例。
const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('findOneAndUpdate', function() {
console.log(this.getFilter()); // { name: 'John' }
console.log(this.getUpdate()); // { age: 30 }
});
const User = mongoose.model('User', userSchema);
await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } });
對於文件中間件,例如 pre('save')
,Mongoose 會將 save()
的第 1 個參數當作 pre('save')
回呼的第 2 個引數傳遞。您應該使用第 2 個引數來存取 save()
呼叫的 options
,因為 Mongoose 文件不會儲存您可以傳遞給 save()
的所有選項。
const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('save', function(next, options) {
options.validateModifiedOnly; // true
// Remember to call `next()` unless you're using an async function or returning a promise
next();
});
const User = mongoose.model('User', userSchema);
const doc = new User({ name: 'John', age: 30 });
await doc.save({ validateModifiedOnly: true });
命名衝突
Mongoose 對於 deleteOne()
都有查詢和文件 hook。
schema.pre('deleteOne', function() { console.log('Removing!'); });
// Does **not** print "Removing!". Document middleware for `deleteOne` is not executed by default
await doc.deleteOne();
// Prints "Removing!"
await Model.deleteOne();
您可以將選項傳遞給 Schema.pre()
和 Schema.post()
,以切換 Mongoose 是否對 Document.prototype.deleteOne()
或 Query.prototype.deleteOne()
呼叫您的 deleteOne()
hook。請注意,在這裡您需要在傳遞的物件中設定 document
和 query
屬性。
// Only document middleware
schema.pre('deleteOne', { document: true, query: false }, function() {
console.log('Deleting doc!');
});
// Only query middleware. This will get called when you do `Model.deleteOne()`
// but not `doc.deleteOne()`.
schema.pre('deleteOne', { query: true, document: false }, function() {
console.log('Deleting!');
});
Mongoose 也對 validate()
有查詢和文件 hook。與 deleteOne
和 updateOne
不同,validate
中介軟體預設會套用到 Document.prototype.validate
。
const schema = new mongoose.Schema({ name: String });
schema.pre('validate', function() {
console.log('Document validate');
});
schema.pre('validate', { query: true, document: false }, function() {
console.log('Query validate');
});
const Test = mongoose.model('Test', schema);
const doc = new Test({ name: 'foo' });
// Prints "Document validate"
await doc.validate();
// Prints "Query validate"
await Test.find().validate();
關於 findAndUpdate()
和查詢中介軟體的注意事項
update()
、findOneAndUpdate()
等不會執行 Pre 和 Post save()
hook。您可以在 此 GitHub 問題中看到更詳細的討論原因。Mongoose 4.0 為這些函數引入了不同的 hook。
schema.pre('find', function() {
console.log(this instanceof mongoose.Query); // true
this.start = Date.now();
});
schema.post('find', function(result) {
console.log(this instanceof mongoose.Query); // true
// prints returned documents
console.log('find() returned ' + JSON.stringify(result));
// prints number of milliseconds the query took
console.log('find() took ' + (Date.now() - this.start) + ' milliseconds');
});
查詢中介軟體與文件中間件的不同之處在於細微但重要的方式:在文件中間件中,this
指的是正在更新的文件。在查詢中介軟體中,mongoose 不一定會有正在更新之文件的參考,因此 this
指的是查詢物件,而不是正在更新的文件。
例如,如果您想要將 updatedAt
時間戳記加入每個 updateOne()
呼叫,您會使用以下 pre hook。
schema.pre('updateOne', function() {
this.set({ updatedAt: new Date() });
});
您無法存取在 pre('updateOne')
或 pre('findOneAndUpdate')
查詢中介軟體中正在更新的文件。如果您需要存取將要更新的文件,您需要執行明確的查詢以取得該文件。
schema.pre('findOneAndUpdate', async function() {
const docToUpdate = await this.model.findOne(this.getQuery());
console.log(docToUpdate); // The document that `findOneAndUpdate()` will modify
});
但是,如果您定義 pre('updateOne')
文件中間件,則 this
會是正在更新的文件。這是因為 pre('updateOne')
文件中間件會掛鉤到 Document#updateOne()
,而不是 Query#updateOne()
。
schema.pre('updateOne', { document: true, query: false }, function() {
console.log('Updating');
});
const Model = mongoose.model('Test', schema);
const doc = new Model();
await doc.updateOne({ $set: { name: 'test' } }); // Prints "Updating"
// Doesn't print "Updating", because `Query#updateOne()` doesn't fire
// document middleware.
await Model.updateOne({}, { $set: { name: 'test' } });
錯誤處理中介軟體
中介軟體執行通常會在一段中介軟體第一次呼叫 next()
並傳入錯誤時停止。但是,有一種特殊類型的 post 中介軟體稱為「錯誤處理中介軟體」,會在發生錯誤時特別執行。錯誤處理中介軟體對於報告錯誤並讓錯誤訊息更具可讀性非常有用。
錯誤處理中介軟體定義為採用一個額外參數的中介軟體:以函數的第一個參數形式發生的「錯誤」。然後,錯誤處理中介軟體可以隨心所欲地轉換錯誤。
const schema = new Schema({
name: {
type: String,
// Will trigger a MongoServerError with code 11000 when
// you save a duplicate
unique: true
}
});
// Handler **must** take 3 parameters: the error that occurred, the document
// in question, and the `next()` function
schema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next();
}
});
// Will trigger the `post('save')` error handler
Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);
錯誤處理中介軟體也適用於查詢中介軟體。您也可以定義一個 post update()
hook,來捕捉 MongoDB 重複金鑰錯誤。
// The same E11000 error can occur when you call `updateOne()`
// This function **must** take 4 parameters.
schema.post('updateOne', function(passRawResult, error, res, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `updateOne()` call will still error out.
}
});
const people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
await Person.create(people);
// Throws "There was a duplicate key error"
await Person.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
錯誤處理中介軟體可以轉換錯誤,但無法移除錯誤。即使您呼叫 next()
而未傳入錯誤(如上所示),函數呼叫仍然會發生錯誤。
彙總 Hook
您也可以為 Model.aggregate()
函數定義 hook。在彙總中介軟體函數中,this
指的是 Mongoose Aggregate
物件。例如,假設您在 Customer
模型上實作軟刪除,方法是新增 isDeleted
屬性。若要確保 aggregate()
呼叫只會查看未被軟刪除的客戶,您可以使用以下中介軟體在每個 彙總管線 的開頭新增 $match
階段。
customerSchema.pre('aggregate', function() {
// Add a $match state to the beginning of each pipeline.
this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });
});
Aggregate#pipeline()
函數可讓您存取 Mongoose 將傳送給 MongoDB 伺服器的 MongoDB 彙總管線。它對於從中介軟體在管線開頭新增階段很有用。
同步 Hook
某些 Mongoose hook 是同步的,這表示它們不支援傳回 promise 或接收 next()
回呼的函數。目前,只有 init
hook 是同步的,因為 init()
函數是同步的。以下是使用 pre 和 post init hook 的範例。
[require:post init hooks.*success]
若要報告 init hook 中的錯誤,您必須拋出一個同步錯誤。與所有其他中介軟體不同,init 中介軟體不會處理 promise 拒絕。
[require:post init hooks.*error]
下一步
既然我們已經介紹過中介軟體,接下來讓我們看看 Mongoose 如何使用其查詢 population 協助程式來偽造 JOIN。