Среднее ПО
Среднее ПО (также называемое пре- и пост-хуками) — это функции, которым передаётся управление во время выполнения асинхронных функций. Среднее ПО задаётся на уровне схемы и полезно для написания плагинов.
Типы Среднего ПО
Mongoose имеет 4 типа среднего ПО: среднее ПО для документов, среднее ПО для моделей, среднее ПО для агрегации и среднее ПО для запросов.
Среднее ПО для документов поддерживается для следующих функций документов. В Mongoose документ является экземпляром класса Model. В функциях среднего ПО для документов this относится к документу. Для доступа к модели используйте this.constructor.
Среднее ПО для запросов поддерживается для следующих функций запросов. Среднее ПО для запросов выполняется при вызове exec() или then() объекта Query, или await объекта Query. В функциях среднего ПО для запросов this относится к запросу.
- count
- countDocuments
- deleteMany
- deleteOne
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndRemove
- findOneAndReplace
- findOneAndUpdate
- remove
- replaceOne
- update
- updateOne
- updateMany
- validate
Среднее ПО для агрегации предназначено для MyModel.aggregate(). Среднее ПО для агрегации выполняется при вызове exec() объекта агрегации. В среднем ПО для агрегации this относится к объекту агрегации.
Среднее ПО для моделей поддерживает следующие функции моделей. Не путайте среднее ПО для моделей и среднее ПО для документов: среднее ПО для моделей подключается к статическим функциям класса Model, среднее ПО для документов подключается к методам класса Model. В функциях среднего ПО для моделей this относится к модели.
Вот возможные строки, которые можно передать в pre()
- aggregate
- count
- countDocuments
- deleteOne
- deleteMany
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndRemove
- findOneAndReplace
- findOneAndUpdate
- init
- insertMany
- remove
- replaceOne
- save
- update
- updateOne
- updateMany
- validate
Все типы среднего ПО поддерживают пре- и пост-хуки. Подробное описание работы пре- и пост-хуков приведено ниже.
Примечание: Если вы зададите schema.pre('remove'), Mongoose по умолчанию зарегистрирует это среднее ПО для doc.remove(). Если вы хотите, чтобы ваше среднее ПО работало с Query.remove(), используйте schema.pre('remove', { query: true, document: false }, fn).
Примечание: В отличие от schema.pre('remove'), Mongoose по умолчанию регистрирует среднее ПО updateOne и deleteOne для Query#updateOne() и Query#deleteOne(). Это означает, что оба doc.updateOne() и Model.updateOne() запускают хуки updateOne, но this относится к запросу, а не к документу. Чтобы зарегистрировать среднее ПО updateOne или deleteOne как среднее ПО для документов, используйте schema.pre('updateOne', { document: true, query: false }).
Примечание: Функция create() запускает хуки save().
Примечание: Среднее ПО для запросов не выполняется для поддокументов.
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
});
Пре
Функции пре-среднего ПО выполняются одна за другой, когда каждое среднее ПО вызывает next.
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
В mongoose 5.x, вместо ручного вызова next() можно использовать функцию, возвращающую промис. В частности, можно использовать async/await.
schema.pre('save', function() {
return doStuff().
then(() => doMoreStuff());
});
// Or, in Node.js >= 7.6.0:
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
Если вы используете next(), вызов next() не останавливает выполнение остальной части кода в вашей функции среднего ПО. Используйте паттерн раннего return, чтобы предотвратить выполнение остальной части вашей функции среднего ПО при вызове next().
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');
});
Примеры использования
Среднее ПО полезно для структурирования логики модели. Вот ещё несколько идей:
- сложная валидация
- удаление зависимых документов (удаление пользователя удаляет все его посты)
- асинхронные значения по умолчанию
- асинхронные задачи, которые запускаются определённым действием
Обработка ошибок в пре-хуках
Если какой-либо пре-хук завершится ошибкой, mongoose не выполнит последующее среднее ПО или связанную функцию. Вместо этого Mongoose передаст ошибку в обратный вызов и/или отклонит возвращённый промис. Существует несколько способов сообщить об ошибке в среднем ПО:
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 среднее ПО выполняется после связанного метода и всех его 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('remove', function(doc) {
console.log('%s has been removed', doc._id);
});
Асинхронные пост-хуки
Если ваша функция пост-хука принимает как минимум 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();
});
Определите среднее ПО до компиляции моделей
Вызов 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(). Нижеприведённый скрипт выведет "Hello from pre save":
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(), потому что в mongoose есть встроенный хук pre('save'), который вызывает validate(). Это означает, что все хуки pre('validate') и post('validate') вызываются до любых хуков pre('save').
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');
});
Доступ к параметрам в среднем ПО
Mongoose предоставляет 2 способа получить информацию о вызове функции, который запустил среднее ПО. Для среднего ПО запросов мы рекомендуем использовать this, который будет экземпляром объекта запроса Mongoose.
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() как второй аргумент в ваш обратный вызов pre('save'). Вы должны использовать второй аргумент для доступа к 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 имеет хуки запросов и документов для remove().
schema.pre('remove', function() { console.log('Removing!'); });
// Prints "Removing!"
doc.remove();
// Does **not** print "Removing!". Query middleware for `remove` is not
// executed by default.
Model.remove();
Вы можете передать параметры в Schema.pre() и Schema.post(), чтобы переключить, вызывает ли Mongoose ваш хук remove() для Document.remove() или Model.remove(). Обратите внимание, что вам нужно установить оба свойства document и query в переданном объекте:
// Only document middleware
schema.pre('remove', { document: true, query: false }, function() {
console.log('Removing doc!');
});
// Only query middleware. This will get called when you do `Model.remove()`
// but not `doc.remove()`.
schema.pre('remove', { query: true, document: false }, function() {
console.log('Removing!');
});
Примечания по findAndUpdate() и среднему ПО для запросов
Пре- и пост-save() хуки не выполняются при использовании update(), findOneAndUpdate(), и т. д. Более подробное обсуждение причин можно найти в этом вопросе на GitHub. Mongoose 4.0 представил отдельные хуки для этих функций.
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(), вы бы использовали следующий пре-хук.
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' } });
Обработка ошибок Middleware
Выполнение мидлвара обычно останавливается в первый раз, когда фрагмент мидлвара вызывает next() с ошибкой. Однако существует особый вид пост-мидлвара, называемый "мидлваром обработки ошибок", который выполняется именно тогда, когда возникает ошибка. Мидлвар обработки ошибок полезен для отчётности об ошибках и для улучшения удобочитаемости сообщений об ошибках.
Мидлвар обработки ошибок определяется как мидлвар, принимающий один дополнительный параметр: "ошибку", которая произошла в качестве первого параметра функции. Затем мидлвар обработки ошибок может преобразовать ошибку так, как вам нужно.
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' }]);
Мидлвар обработки ошибок также работает с мидлваром запросов. Вы также можете определить пост update() хук, который будет перехватывать ошибки дублирования ключей MongoDB.
// The same E11000 error can occur when you call `update()`
// This function **must** take 3 parameters. If you use the
// `passRawResult` function, this function **must** take 4
// parameters
schema.post('update', function(error, res, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `update()` 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.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
Мидлвар обработки ошибок может преобразовать ошибку, но не может её удалить. Даже если вы вызываете next() без ошибки, как показано выше, вызов функции всё равно завершится ошибкой.
Хук агрегации
Вы также можете определить хуки для функции Model.aggregate(). В функциях агрегации мидлвара 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() позволяет получить доступ к агрегационному конвейеру MongoDB, который Mongoose отправит серверу MongoDB. Это полезно для добавления этапов в начало конвейера из мидлвара.
Синхронные хуки
Некоторые хуки Mongoose являются синхронными, что означает, что они не поддерживают функции, которые возвращают промисы или принимают next() коллбек. В настоящее время только init хуки являются синхронными, потому что функция init() является синхронной. Ниже приведен пример использования пре- и пост-хуков init.
const schema = new Schema({ title: String, loadedAt: Date });
schema.pre('init', pojo => {
assert.equal(pojo.constructor.name, 'Object'); // Plain object before init
});
const now = new Date();
schema.post('init', doc => {
assert.ok(doc instanceof mongoose.Document); // Mongoose doc after init
doc.loadedAt = now;
});
const Test = db.model('Test', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
then(doc => assert.equal(doc.loadedAt.valueOf(), now.valueOf()));
Чтобы сообщить об ошибке в хуке init, вы должны бросить синхронную ошибку. В отличие от всех других мидлваров, мидлвар init не обрабатывает отклонения промисов.
const schema = new Schema({ title: String });
const swallowedError = new Error('will not show');
// init hooks do **not** handle async errors or any sort of async behavior
schema.pre('init', () => Promise.reject(swallowedError));
schema.post('init', () => { throw Error('will show'); });
const Test = db.model('Test', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
catch(error => assert.equal(error.message, 'will show'));
Далее
Теперь, когда мы рассмотрели мидлвар, давайте взглянем на подход Mongoose к имитации JOIN с помощью помощника по обработке запросов популяции.
© 2010 LearnBoost
Licensed under the MIT License.
https://mongoosejs.com/docs/middleware.html