Система хуков в Dexie

Система хуков в Dexie.js построена как механизм перехвата жизненного цикла операций над данными и позволяет внедрять дополнительную логику в ключевые точки работы с IndexedDB без изменения бизнес-кода. Хуки привязываются к таблицам или ко всей базе и срабатывают синхронно или асинхронно в зависимости от типа операции, обеспечивая контроль над созданием, чтением, обновлением и удалением записей.

Хуки в Dexie.js реализованы как подписки на события низкоуровневых операций таблиц. Каждая таблица имеет собственный набор событий:

  • creating — перед добавлением записи
  • reading — при чтении записи из хранилища
  • updating — перед обновлением записи
  • deleting — перед удалением записи

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

Ключевая особенность заключается в том, что хуки работают ближе к уровню IndexedDB, чем методы коллекций, что делает их полезными для реализации сквозной логики: аудита, валидации, нормализации данных и каскадных операций.

Регистрация хуков

Хуки регистрируются через метод db.table.hook():

db.users.hook('creating', function (primKey, obj, transaction) {
    obj.createdAt = Date.now();
});

Первый аргумент — тип события, второй — обработчик.

Также можно регистрировать несколько обработчиков на одно событие — они будут выполняться последовательно.

Контекст выполнения хука

Каждый хук получает доступ к:

  • первичному ключу (primKey)
  • объекту данных (obj)
  • транзакции (transaction)

В некоторых случаях доступны дополнительные параметры, например для updating — набор модификаций.

Контекст транзакции позволяет выполнять дополнительные запросы в рамках той же атомарной операции.

Хук creating

Срабатывает до добавления записи в IndexedDB. Используется для:

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

Пример:

db.users.hook('creating', function (primKey, obj, transaction) {
    if (!obj.email) {
        throw new Error('Email обязателен');
    }

    obj.createdAt = new Date().toISOString();
    obj.isActive = true;
});

Особенность: изменение obj здесь напрямую влияет на сохраняемую запись.

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

Хук reading

Срабатывает при чтении каждой записи из базы, включая запросы через индексы и курсоры.

Используется для:

  • виртуальных полей
  • маскирования данных
  • преобразования формата
db.users.hook('reading', function (obj) {
    obj.fullName = obj.firstName + ' ' + obj.lastName;
    delete obj.passwordHash;
});

Важно, что изменения в reading не сохраняются в базе, а влияют только на возвращаемый результат.

Хук updating

Срабатывает перед применением изменений к существующей записи.

Передаёт объект модификаций, который может быть изменён:

db.users.hook('updating', function (modifications, primKey, obj, transaction) {
    modifications.updatedAt = Date.now();

    if (modifications.email && !modifications.email.includes('@')) {
        throw new Error('Некорректный email');
    }
});

Особенность: modifications содержит только изменяемые поля, а не полную запись.

Это делает хук удобным для частичных обновлений и патчей.

Хук deleting

Срабатывает перед удалением записи:

db.users.hook('deleting', function (primKey, obj, transaction) {
    if (obj.role === 'admin') {
        throw new Error('Удаление администратора запрещено');
    }
});

Хук получает полную запись, что позволяет принимать решения на основе всех данных объекта.

Асинхронные хуки

Dexie.js поддерживает асинхронные операции внутри хуков через возврат Promise.

db.users.hook('creating', async function (primKey, obj, transaction) {
    const exists = await db.users.where('email').equals(obj.email).first();

    if (exists) {
        throw new Error('Email уже используется');
    }
});

Асинхронность особенно полезна при необходимости дополнительных проверок или обращения к другим таблицам.

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

Если зарегистрировано несколько хуков одного типа, они выполняются в порядке регистрации.

db.users.hook('creating', fn1);
db.users.hook('creating', fn2);

Сначала выполнится fn1, затем fn2.

Любой из хуков может прервать цепочку, выбросив исключение.

Глобальные хуки базы данных

Помимо хуков таблиц, существуют глобальные хуки уровня базы:

db.hook('ready', function () {
    console.log('База готова');
});

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

Использование транзакции внутри хуков

Транзакция, передаваемая в хук, позволяет выполнять дополнительные операции без нарушения атомарности:

db.orders.hook('creating', async function (primKey, order, tx) {
    const user = await tx.table('users').get(order.userId);

    if (!user.isActive) {
        throw new Error('Пользователь не активен');
    }
});

Это обеспечивает согласованность данных между таблицами.

Каскадные операции через хуки

Хуки часто используются для реализации каскадного поведения, например удаления связанных данных:

db.users.hook('deleting', async function (primKey, user, tx) {
    await tx.table('orders')
        .where('userId')
        .equals(primKey)
        .delete();
});

Такой подход позволяет эмулировать поведение SQL ON DELETE CASCADE.

Ограничения системы хуков

Несмотря на гибкость, система имеет ряд особенностей:

  • хуки не заменяют транзакционную логику
  • сложные цепочки могут усложнить отладку
  • чтение (reading) не влияет на сохранённые данные
  • нельзя гарантировать параллельную безопасность вне транзакции

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

Влияние хуков на производительность

Каждый хук добавляет накладные расходы к операции. Особенно это заметно при:

  • массовых вставках
  • сложных асинхронных проверках
  • цепочках хуков на одной таблице

Оптимизация достигается за счёт:

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

Композиция логики через хуки

Хуки позволяют строить модульную архитектуру, где разные аспекты логики разделены:

  • модуль валидации
  • модуль аудита
  • модуль синхронизации

Пример композиции:

db.users.hook('creating', validateUser);
db.users.hook('creating', addTimestamps);
db.users.hook('creating', logCreation);

Каждый модуль отвечает за свою задачу, не вмешиваясь в другие.

Отладка хуков

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

Типичная стратегия отладки включает:

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

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