Хук deleting

Dexie.js предоставляет мощную систему хуков (hooks), позволяющую вмешиваться в жизненный цикл операций над данными в IndexedDB. Одним из ключевых механизмов является хук удаления (deleting hook), который вызывается перед выполнением операции удаления записи из таблицы. Этот хук используется для контроля, модификации логики удаления, каскадного удаления связанных данных, ведения аудита и реализации сложных бизнес-правил.

Операции с данными в Dexie проходят через несколько этапов:

  • подготовка запроса
  • выполнение транзакции
  • выполнение операции над IndexedDB
  • завершение транзакции

Хук deleting срабатывает до фактического удаления записи, но уже внутри транзакционного контекста. Это означает, что:

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

Поддерживается поведение:

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

Сигнатура и регистрация deleting-хука

Хук регистрируется на уровне таблицы:

db.table('users').hook('deleting', function (primKey, obj, transaction) {
    // логика перед удалением
});

Параметры:

  • primKey — первичный ключ удаляемой записи
  • obj — полная запись, если она доступна
  • transaction — текущая транзакция Dexie

Контекст this внутри хука указывает на таблицу, из которой производится удаление.

Особенности поведения obj в deleting-хуке

Объект obj может быть:

  • полным объектом записи, если он уже загружен
  • undefined, если Dexie не загрузил объект заранее (например, при delete(key) без предварительного чтения)

Это приводит к важному выводу: нельзя полагаться на наличие obj без дополнительного запроса. При необходимости гарантированного доступа к данным используется явное чтение внутри транзакции:

db.table('users').hook('deleting', async function (primKey, obj, transaction) {
    const user = obj || await transaction.table('users').get(primKey);
});

Прерывание удаления

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

Для этого используется выброс исключения:

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

При выбросе исключения:

  • транзакция откатывается
  • удаление не выполняется
  • вызывающий код получает ошибку

Асинхронные операции в deleting-хуке

Dexie поддерживает асинхронные хуки. Это позволяет выполнять проверки и дополнительные запросы перед удалением:

db.table('orders').hook('deleting', async function (id, order, transaction) {
    const payments = await transaction.table('payments')
        .where('orderId')
        .equals(id)
        .toArray();

    if (payments.length > 0) {
        throw new Error('Нельзя удалить заказ с платежами');
    }
});

Асинхронность важна при:

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

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

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

Пример каскадного удаления:

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

    await tx.table('notifications')
        .where('userId')
        .equals(userId)
        .delete();
});

Такая модель гарантирует:

  • атомарность всех удалений
  • отсутствие частично удалённых данных
  • согласованность состояния базы

Сценарии применения deleting-хука

Каскадное удаление

В IndexedDB отсутствуют встроенные foreign key constraints, поэтому каскады реализуются вручную:

db.table('posts').hook('deleting', async function (postId, post, tx) {
    await tx.table('comments')
        .where('postId')
        .equals(postId)
        .delete();
});

Логирование удалений

Хук позволяет фиксировать историю операций:

db.table('products').hook('deleting', function (id, product, tx) {
    return tx.table('audit').add({
        type: 'DELETE',
        table: 'products',
        key: id,
        snapshot: product,
        timestamp: Date.now()
    });
});

Проверка прав доступа

db.table('files').hook('deleting', function (id, file) {
    if (!file || file.owner !== currentUserId) {
        throw new Error('Нет прав на удаление файла');
    }
});

Защита критических данных

db.table('settings').hook('deleting', function (id, setting) {
    if (setting && setting.protected) {
        throw new Error('Системные настройки нельзя удалять');
    }
});

Особенности выполнения и порядок вызова

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

Поведение:

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

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

Взаимодействие с другими хуками

Dexie поддерживает также:

  • creating
  • updating
  • reading

Хук deleting отличается тем, что:

  • работает только перед удалением
  • не имеет post-hook аналога в стандартной системе
  • часто комбинируется с creating и updating для полной модели контроля данных

Например, совместное использование:

db.table('logs').hook('deleting', function () {
    throw new Error('Логи нельзя удалять');
});

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

Ограничения deleting-хука

Несмотря на гибкость, существуют ограничения:

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

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

Поведение при массовых удалениях

При вызове:

db.table('items').where('type').equals('temp').delete();

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

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

Для оптимизации в таких сценариях используют:

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

Практическая модель использования

Deleting-хук в Dexie.js фактически формирует слой прикладной бизнес-логики поверх низкоуровневого IndexedDB. Он позволяет эмулировать:

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

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