Sequelize: хуки beforeCreate и beforeUpdate

В Sequelize хуки представляют собой функции, которые автоматически вызываются на определённых этапах жизненного цикла модели. Они позволяют вмешиваться в процесс создания, обновления, удаления и других операций с данными, не нарушая архитектуру приложения и не дублируя бизнес-логику в контроллерах.

Особое значение имеют хуки beforeCreate и beforeUpdate, так как именно они используются для модификации данных до их сохранения в базу.


Общая концепция хуков

Хуки в Sequelize работают как промежуточные обработчики событий. Они вызываются автоматически в момент выполнения определённых операций над моделью.

Основные категории:

  • создание записи
  • обновление записи
  • удаление записи
  • валидация
  • загрузка данных

beforeCreate и beforeUpdate относятся к pre-hooks — они срабатывают до выполнения SQL-запроса.


beforeCreate

Хук beforeCreate вызывается перед созданием новой записи в базе данных.

Сигнатура

Model.beforeCreate(async (instance, options) => {
  // логика
});

Параметры

  • instance — экземпляр модели, который будет сохранён
  • options — объект с дополнительными параметрами запроса

Типовые сценарии использования beforeCreate

Хеширование пароля

Один из самых частых кейсов — обработка пароля перед сохранением:

import bcrypt from 'bcrypt';

User.beforeCreate(async (user) => {
  if (user.password) {
    const salt = await bcrypt.genSalt(10);
    user.password = await bcrypt.hash(user.password, salt);
  }
});

В этом случае исходный пароль никогда не сохраняется в базе в открытом виде.


Генерация дополнительных полей

User.beforeCreate((user) => {
  user.createdAtTimestamp = Date.now();
});

Нормализация данных

User.beforeCreate((user) => {
  user.email = user.email.toLowerCase().trim();
});

Особенности beforeCreate

  • вызывается только при Model.create()
  • не срабатывает при bulkCreate без индивидуальных hooks (если не включено individualHooks: true)
  • можно использовать асинхронные операции
  • изменения в instance автоматически попадают в INSERT-запрос

beforeUpdate

Хук beforeUpdate вызывается перед обновлением существующей записи.

Сигнатура

Model.beforeUpdate(async (instance, options) => {
  // логика
});

Отличие от beforeCreate

Главное отличие заключается в том, что:

  • beforeCreate работает только с новыми записями
  • beforeUpdate работает только с уже существующими

При этом оба хука имеют доступ к экземпляру модели и могут изменять его перед сохранением.


Типовые сценарии beforeUpdate

Обновление пароля

import bcrypt from 'bcrypt';

User.beforeUpdate(async (user) => {
  if (user.changed('password')) {
    const salt = await bcrypt.genSalt(10);
    user.password = await bcrypt.hash(user.password, salt);
  }
});

Ключевой момент — проверка changed(), чтобы избежать повторного хеширования уже хешированного значения.


Автоматическое обновление метаданных

User.beforeUpdate((user) => {
  user.updatedAtTimestamp = Date.now();
});

Валидация состояния перед сохранением

User.beforeUpdate((user) => {
  if (user.status === 'banned' && !user.reason) {
    throw new Error('Причина блокировки обязательна');
  }
});

Если в хуке выбрасывается исключение, операция обновления отменяется.


Важный механизм: changed()

Метод changed() используется для определения, изменилось ли поле:

user.changed('email')

или

user.changed()

(вернёт список изменённых полей)


Особенности beforeUpdate

  • срабатывает только при save() или update()
  • не срабатывает при bulkUpdate
  • требует осторожности при массовых операциях
  • может отменить обновление через исключение

Порядок выполнения хуков

При обновлении:

  1. beforeValidate
  2. beforeUpdate
  3. SQL UPDATE
  4. afterUpdate

При создании:

  1. beforeValidate
  2. beforeCreate
  3. SQL INSERT
  4. afterCreate

Взаимодействие с bcrypt.js в хуках

Часто хуки используются совместно с bcrypt.js для безопасного хранения паролей.

Полный пример модели

import bcrypt from 'bcrypt';

export default (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    email: DataTypes.STRING,
    password: DataTypes.STRING
  });

  User.beforeCreate(async (user) => {
    const salt = await bcrypt.genSalt(10);
    user.password = await bcrypt.hash(user.password, salt);
  });

  User.beforeUpdate(async (user) => {
    if (user.changed('password')) {
      const salt = await bcrypt.genSalt(10);
      user.password = await bcrypt.hash(user.password, salt);
    }
  });

  return User;
};

Частые ошибки при использовании хуков

1. Повторное хеширование пароля

Если не проверять changed('password'), можно случайно зашифровать уже зашифрованное значение.


2. Использование bulkCreate без individualHooks

User.bulkCreate(users, { individualHooks: true });

Без этого хуки не сработают для каждого элемента.


3. Синхронный код в асинхронном хукe

Если используется bcrypt или другие async операции, необходимо всегда использовать async/await.


4. Изменение данных вне instance

Нельзя рассчитывать на мутацию внешних переменных — только instance.


Контроль выполнения хуков

Sequelize позволяет отключать хуки:

User.update(
  { name: 'Test' },
  { hooks: false }
);

Это полезно для системных операций, но опасно для критичных данных.


Архитектурная роль beforeCreate и beforeUpdate

Эти хуки позволяют:

  • централизовать бизнес-логику
  • исключить дублирование кода
  • повысить безопасность данных
  • контролировать целостность модели

Они часто используются как слой доменной логики, который работает независимо от контроллеров и сервисов.