Система хуков в Dexie.js построена как механизм перехвата жизненного цикла операций над данными и позволяет внедрять дополнительную логику в ключевые точки работы с IndexedDB без изменения бизнес-кода. Хуки привязываются к таблицам или ко всей базе и срабатывают синхронно или асинхронно в зависимости от типа операции, обеспечивая контроль над созданием, чтением, обновлением и удалением записей.
Хуки в Dexie.js реализованы как подписки на события низкоуровневых операций таблиц. Каждая таблица имеет собственный набор событий:
Каждый хук получает контекст операции и может модифицировать данные или прервать выполнение через выброс исключения.
Ключевая особенность заключается в том, что хуки работают ближе к уровню IndexedDB, чем методы коллекций, что делает их полезными для реализации сквозной логики: аудита, валидации, нормализации данных и каскадных операций.
Хуки регистрируются через метод db.table.hook():
db.users.hook('creating', function (primKey, obj, transaction) {
obj.createdAt = Date.now();
});
Первый аргумент — тип события, второй — обработчик.
Также можно регистрировать несколько обработчиков на одно событие — они будут выполняться последовательно.
Каждый хук получает доступ к:
primKey)obj)transaction)В некоторых случаях доступны дополнительные параметры, например для
updating — набор модификаций.
Контекст транзакции позволяет выполнять дополнительные запросы в рамках той же атомарной операции.
Срабатывает до добавления записи в 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 здесь напрямую влияет на
сохраняемую запись.
Если хук выбрасывает исключение, операция вставки отменяется.
Срабатывает при чтении каждой записи из базы, включая запросы через индексы и курсоры.
Используется для:
db.users.hook('reading', function (obj) {
obj.fullName = obj.firstName + ' ' + obj.lastName;
delete obj.passwordHash;
});
Важно, что изменения в reading не сохраняются в базе, а
влияют только на возвращаемый результат.
Срабатывает перед применением изменений к существующей записи.
Передаёт объект модификаций, который может быть изменён:
db.users.hook('updating', function (modifications, primKey, obj, transaction) {
modifications.updatedAt = Date.now();
if (modifications.email && !modifications.email.includes('@')) {
throw new Error('Некорректный email');
}
});
Особенность: modifications содержит только изменяемые
поля, а не полную запись.
Это делает хук удобным для частичных обновлений и патчей.
Срабатывает перед удалением записи:
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.