MongoDB Academy · Урок

Промежуточное ПО Mongoose: предварительные и последующие хуки

Вы напишете хуки промежуточного ПО для документов и запросов, например для хеширования паролей перед сохранением или ведения журнала после поиска.

Урок 4 из 413 шагов

«Промежуточное ПО 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 avatar

Pre-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 — локальная установка не требуется.

Все уроки этого курса

  1. Подключение с помощью официального драйвера Node.js
  2. Схемы, модели и виртуальные свойства Mongoose
  3. Запросы Mongoose, цепочки и документы без преобразования
  4. Промежуточное ПО Mongoose: предварительные и последующие хуки
← Назад к MongoDB Academy