Хук updating

Хук updating в Dexie.js перехватывает момент изменения записи в таблице до фактического применения обновления в IndexedDB. Он используется для контроля, трансформации или блокировки изменений на уровне бизнес-логики, позволяя централизованно управлять тем, как именно модифицируются данные при вызове update() или при частичном обновлении через put().

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

  • целевую таблицу;
  • первичный ключ обновляемой записи;
  • набор изменений, которые будут применены;
  • текущий объект записи (до модификации);
  • транзакцию, в рамках которой выполняется операция.

Сигнатура:

table.hook('updating', function (modifications, primaryKey, obj, transaction) {
    // логика хука
});

Параметры хука

modifications Объект, содержащий только изменяемые поля. Не является полной записью. Именно эти данные будут наложены поверх существующего объекта.

Пример:

{ name: "New name", age: 30 }

Важно учитывать, что это не клон всей записи, а дифф.

primaryKey Первичный ключ обновляемой записи. Используется для идентификации объекта, но не гарантирует наличие полной структуры записи.

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

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


Механика возврата значения

Хук может управлять итоговыми изменениями через возвращаемое значение.

Если возвращается объект — он заменяет исходный modifications.

table.hook('updating', (mods, key, obj) => {
    return {
        ...mods,
        updatedAt: Date.now()
    };
});

Если возвращается undefined, изменения применяются без модификации.

table.hook('updating', (mods) => {
    if (mods.name === undefined) return;
});

Также возможно полное блокирование изменения через выброс исключения:

table.hook('updating', () => {
    throw new Error('Обновление запрещено');
});

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

Валидация изменений

Хук часто используется для проверки допустимости изменений до записи в IndexedDB.

db.users.hook('updating', (mods, key, obj) => {
    if (mods.age !== undefined && mods.age < 0) {
        throw new Error('Возраст не может быть отрицательным');
    }
});

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


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

Распространённый паттерн — автоматическое обновление метаданных.

db.posts.hook('updating', (mods) => {
    return {
        ...mods,
        updatedAt: new Date().toISOString()
    };
});

Таким образом исключается необходимость вручную обновлять временные метки в каждом update().


Контроль неизменяемых полей

Некоторые поля могут быть защищены от изменения.

db.users.hook('updating', (mods, key, obj) => {
    if ('role' in mods) {
        throw new Error('Поле role нельзя изменять');
    }
});

Альтернативный подход — принудительное игнорирование:

db.users.hook('updating', (mods) => {
    const { role, ...rest } = mods;
    return rest;
});

Денормализация связанных данных

Хук используется для синхронизации производных данных в связанных таблицах или кэшированных полях.

db.orders.hook('updating', (mods, key, obj, tx) => {
    if (mods.status && mods.status !== obj.status) {
        tx.table('orderLogs').add({
            orderId: key,
            from: obj.status,
            to: mods.status,
            changedAt: Date.now()
        });
    }
});

Здесь транзакция обеспечивает согласованность между таблицами.


Работа с частичными обновлениями

Dexie передаёт в modifications только изменённые поля. Это создаёт важную особенность: отсутствие поля не означает его неизменность, а означает, что оно не затрагивается.

db.items.hook('updating', (mods, key, obj) => {
    // obj содержит полную запись
    // mods содержит только изменённые поля
});

Для вычисления итогового состояния требуется объединение:

const nextState = { ...obj, ...mods };

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

Dexie допускает возврат Promise из хука, что позволяет выполнять асинхронные преобразования или проверки.

db.users.hook('updating', async (mods, key, obj) => {
    if (mods.email) {
        const exists = await db.users
            .where('email')
            .equals(mods.email)
            .and(u => u.id !== key)
            .count();

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

    return mods;
});

Асинхронность увеличивает гибкость, но сохраняет транзакционную целостность операции.


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

Изменения, возвращаемые из хука, напрямую влияют на данные, которые попадут в IndexedDB. Любая дополнительная логика внутри updating выполняется в критическом пути записи.

Ключевые аспекты:

  • синхронные вычисления выполняются быстрее и предсказуемее;
  • асинхронные проверки увеличивают задержку транзакции;
  • чрезмерная логика в хуке может замедлить массовые обновления;
  • модификация больших объектов требует осторожности из-за копирования.

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

Dexie предоставляет несколько хуков уровня таблицы:

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

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


Изменение структуры данных на лету

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

db.users.hook('updating', (mods, key, obj) => {
    if (mods.fullName) {
        const [firstName, lastName] = mods.fullName.split(' ');
        return {
            ...mods,
            firstName,
            lastName
        };
    }
});

Такой подход часто применяется при постепенной эволюции схемы хранения данных.


Ограничения и особенности поведения

  • Хук не вызывается при прямой записи низкоуровневыми API IndexedDB вне Dexie.
  • Порядок выполнения нескольких хуков определяется порядком регистрации.
  • Возврат нового объекта полностью заменяет изменения, но не оригинальную запись.
  • Изменение primaryKey невозможно через updating-хук для большинства схем.
  • Ошибки внутри хука отменяют всю транзакцию.

Композиция нескольких хуков

При наличии нескольких updating-хуков важно учитывать порядок трансформаций:

db.table.hook('updating', (mods) => {
    return { ...mods, step1: true };
});

db.table.hook('updating', (mods) => {
    return { ...mods, step2: true };
});

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


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

Хук updating часто становится центральной точкой инкапсуляции бизнес-правил на уровне локальной базы данных. Он позволяет:

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

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