Промежуточное ПО Mongoose: предварительные и последующие хуки
Вы напишете хуки промежуточного ПО для документов и запросов, например для хеширования паролей перед сохранением или ведения журнала после поиска.
«Промежуточное ПО Mongoose: предварительные и последующие хуки» — бесплатный урок MongoDB Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения MongoDB Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс MongoDB Academy содержит 4 уроков всего.
Что такое промежуточное ПО Mongoose?
Промежуточное ПО Mongoose (также называемое хуками) — это функции, которые выполняются до или после определённых операций, таких как save, find, updateOne, deleteOne и других. Оно позволяет добавлять пользовательскую логику в жизненный цикл операций с документами и запросами, не загромождая обработчики маршрутов. Типичные применения: хеширование паролей перед сохранением, журналирование времени выполнения запросов, реализация мягкого удаления и заполнение связанных данных после поиска.
Два типа: промежуточное ПО документов и запросов
В Mongoose есть две отдельные категории промежуточного ПО: промежуточное ПО документов подключается к операциям над конкретным экземпляром документа (save, validate, remove, init). Промежуточное ПО запросов подключается к операциям запросов, вызываемым для модели (find, findOne, updateOne, deleteOne и т. д.). Ключевое различие заключается в том, на что ссылается this: в промежуточном ПО документов это документ, а в промежуточном ПО запросов — объект запроса.
// Document middleware: 'this' = the document
userSchema.pre('save', function () {
console.log('Saving document:', this.email);
});
// Query middleware: 'this' = the Query object
userSchema.pre('find', function () {
console.log('Running query:', this.getQuery());
});Pre-save: хеширование паролей
Хук pre-save — наиболее распространённый вид промежуточного ПО документов. Он выполняется перед сохранением документа в MongoDB. Классический пример использования — хеширование паролей: когда документ пользователя сохраняется с новым или изменённым паролем, перед сохранением хешируйте его с помощью bcrypt. Проверка this.isModified('password') предотвращает повторное хеширование уже хешированного пароля при несвязанных сохранениях.
const bcrypt = require('bcrypt');
userSchema.pre('save', async function () {
// 'this' is the User document being saved
if (!this.isModified('password')) {
return; // skip if password hasn't changed
}
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
// The hashed value replaces the plain password before MongoDB stores it
});Вспомогательные свойства isNew и isModified
Промежуточное ПО документов имеет доступ к вспомогательным средствам отслеживания: this.isNew равно true, когда документ вставляется впервые, а не обновляется. this.isModified(path) возвращает true, если указанное поле изменилось с момента последнего сохранения или получения документа. Эти средства позволяют выполнять хуки условно — только при создании или только при изменении определённого поля.
userSchema.pre('save', async function () {
if (this.isNew) {
// Only runs when creating a new user, not on updates
this.verificationToken = crypto.randomBytes(32).toString('hex');
this.verificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
}
if (this.isModified('email')) {
// Only runs when the email field specifically changed
this.emailVerified = false; // reset verification on email change
}
});Post-save: побочные эффекты после сохранения
Хуки post-save выполняются после успешного сохранения документа. Они получают сохранённый документ в качестве первого аргумента и (в старых версиях Mongoose) callback next. Хуки post идеально подходят для побочных эффектов, которые должны происходить после успешного сохранения: отправки приветственного письма, обновления поискового индекса, публикации события в очередь сообщений или очистки кэша. Ошибки в хуках post не откатывают сохранение.
userSchema.post('save', async function (doc) {
// 'doc' is the saved document
if (doc.isNew) {
// Note: 'isNew' is false here (doc was just saved, so it's no longer new)
// Track this with a flag set in pre-save:
}
});
// Pattern: set a flag in pre-save, read it in post-save
userSchema.pre('save', function () {
this._wasNew = this.isNew; // save state before it changes
});
userSchema.post('save', async function (doc) {
if (doc._wasNew) {
await sendWelcomeEmail(doc.email, doc.name);
await analyticsTracker.track('user_created', { userId: doc._id });
}
});Промежуточное ПО запросов: pre-find для мягкого удаления
Классический пример промежуточного ПО запросов — реализация мягкого удаления. Вместо удаления документов устанавливайте поле deletedAt. Затем добавьте хук pre('find'), который автоматически добавляет { deletedAt: null } в каждый запрос поиска, чтобы удалённые документы по умолчанию никогда не возвращались. Это сохраняет историю изменений и одновременно делает логику мягкого удаления прозрачной для остальной части приложения.
const postSchema = new mongoose.Schema({
title: String,
content: String,
deletedAt: { type: Date, default: null }
});
// Automatically exclude soft-deleted documents from all find queries
postSchema.pre(/^find/, function () {
// 'this' is the Query object
this.where({ deletedAt: null });
// /^find/ matches find, findOne, findOneAndUpdate, etc.
});
// Now Post.find({}) never returns deleted posts
// To explicitly query deleted posts, you'd call Post.find({}).bypassMiddleware() or use .lean() with the native driverПромежуточное ПО запросов: автоматическое заполнение при pre-find
Промежуточное ПО запросов можно использовать для автоматического заполнения поля-ссылки при каждом поиске. Это гарантирует разрешение ссылки на документ без необходимости добавлять .populate() в каждый запрос. Хотя это удобно, будьте осторожны: автоматическое заполнение добавляет второй запрос при каждом поиске и может снизить производительность, если связанный объект большой или нужен не всегда.
const reviewSchema = new mongoose.Schema({
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
rating: Number,
comment: String
});
// Always populate author info on find
reviewSchema.pre(/^find/, function () {
this.populate({
path: 'userId',
select: 'name avatar'
});
});
// Now Review.find() always includes user name and avatarPre-deleteOne: каскадное удаление
Mongoose автоматически не обеспечивает каскадное удаление (удаление связанных документов при удалении родительского документа). Вы можете реализовать каскадное поведение с помощью промежуточного ПО документов. Хук pre-deleteOne для модели User может удалить все публикации, комментарии и сеансы, принадлежащие этому пользователю, прежде чем будет удалён сам документ пользователя. Это сохраняет ссылочную целостность без ограничений внешних ключей.
userSchema.pre('deleteOne', { document: true, query: false }, async function () {
// 'this' is the User document being deleted
const userId = this._id;
// Cascade delete related documents
await Promise.all([
Post.deleteMany({ authorId: userId }),
Comment.deleteMany({ userId: userId }),
Session.deleteMany({ userId: userId }),
Notification.deleteMany({ userId: userId })
]);
console.log('Cascade deleted data for user:', userId);
});
// Trigger:
// const user = await User.findById(id);
// await user.deleteOne(); // triggers pre-deleteOne aboveОбработка ошибок в промежуточном ПО
Если функция pre-hook выбрасывает ошибку или отклоняет Promise, предшествующая ей операция прерывается. Это позволяет выполнять проверки данных или авторизации в промежуточном ПО и прерывать сохранение или запрос с помощью выбрасывания ошибки. Например, хук pre-save, проверяющий бизнес-логику, а не только схему, может выбросить ошибку, которая попадёт в блок catch вызова .save() в коде приложения.
orderSchema.pre('save', async function () {
if (this.total <= 0) {
throw new Error('Order total must be positive');
}
// Check inventory synchronously before saving the order
const product = await Product.findById(this.productId).lean();
if (!product || product.stock < this.quantity) {
throw new Error('Insufficient inventory for this order');
}
});
// In route handler:
try {
const order = new Order({ productId, quantity, total });
await order.save(); // throws if pre-save hook rejects
} catch (err) {
res.status(400).json({ error: err.message });
}Промежуточное ПО агрегации
Mongoose также поддерживает промежуточное ПО для конвейеров агрегации. Хук pre-aggregate предоставляет доступ к массиву конвейера до его отправки в MongoDB, позволяя добавлять этапы в начало (например, фильтрацию удалённых документов) или в конец (например, добавление ограничения по умолчанию). Получить доступ к конвейеру внутри функции хука можно через this.pipeline().
postSchema.pre('aggregate', function () {
// 'this' is the Aggregate object
// Add a $match stage at the beginning to exclude soft-deleted documents
this.pipeline().unshift({
$match: { deletedAt: null }
});
});
// Now Post.aggregate([...]) automatically excludes deleted posts
// at the start of every aggregation pipelineНедостатки промежуточного ПО: методы запросов, обходящие хуки
Не все операции записи запускают промежуточное ПО документов. Вызванные для модели (а не экземпляра) updateMany(), findOneAndUpdate() и replaceOne() обходят хуки save документов — это промежуточное ПО запросов, и для их перехвата нужны отдельные хуки. Например, хук pre-save для хеширования пароля НЕ запускается при вызове User.updateOne({}, { $set: { password: plain } }). При обновлениях через запрос всегда хешируйте пароль в коде приложения.
// WRONG: password NOT hashed — bypasses pre-save hook
await User.updateOne({ _id: userId }, { $set: { password: plainPassword } });
// RIGHT for query-level updates: hash before calling updateOne
const hashed = await bcrypt.hash(plainPassword, 12);
await User.updateOne({ _id: userId }, { $set: { password: hashed } });
// Or: fetch, modify, save — triggers pre-save hook
const user = await User.findById(userId);
user.password = plainPassword; // hook will hash it
await user.save();Быстрая проверка
Проверьте, насколько хорошо Вы поняли концепции MongoDB и баз данных NoSQL из этого урока.
Итоги урока
В этом уроке Вы узнали, что хуки pre выполняются до операции и могут прервать её, выбросив ошибку, а хуки post выполняются после операции и получают результат в качестве аргумента, промежуточное ПО документов (pre-save, pre-deleteOne) использует 'this' как документ, а промежуточное ПО запросов — как объект Query, и методы записи на уровне запросов (updateOne, updateMany, findOneAndUpdate) обходят промежуточное ПО документов — всегда учитывайте, какие хуки срабатывают для каждого типа операции. На этом завершается учебный курс по MongoDB и базам данных NoSQL!
Изучай JavaScript с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 30
- Уроки
- 120
Часто задаваемые вопросы
Урок «Промежуточное ПО Mongoose: предварительные и последующие хуки» бесплатный?
Да — полный текст урока «Промежуточное ПО Mongoose: предварительные и последующие хуки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс MongoDB Academy, подпишись на CoddyKit PRO. Курс MongoDB Academy содержит 4 уроков всего.
Чему я научусь в уроке «Промежуточное ПО Mongoose: предварительные и последующие хуки»?
Вы напишете хуки промежуточного ПО для документов и запросов, например для хеширования паролей перед сохранением или ведения журнала после поиска. Ты практикуешь MongoDB Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать MongoDB Academy?
Предыдущий опыт не требуется. MongoDB Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Промежуточное ПО Mongoose: предварительные и последующие хуки»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке MongoDB Academy?
Да. Каждый урок MongoDB Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Подключение с помощью официального драйвера Node.js
- Схемы, модели и виртуальные свойства Mongoose
- Запросы Mongoose, цепочки и документы без преобразования
- Промежуточное ПО Mongoose: предварительные и последующие хуки