Хук reading

В Dexie.js механизм хуков чтения относится к внутреннему этапу преобразования данных в момент их извлечения из IndexedDB и передачи в пользовательский код. Этот тип хука позволяет перехватывать каждый объект, который возвращается из таблицы, и модифицировать его до того, как он попадёт в результат get, toArray, each, where().toArray() и других методов чтения.

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


Регистрация reading hook

Dexie предоставляет доступ к хукам через db.table.hook('reading', ...). Общая форма регистрации выглядит следующим образом:

db.users.hook('reading', (obj) => {
    return obj;
});

Каждый объект, возвращённый из таблицы users, проходит через этот обработчик.


Сигнатура и поведение

Хук чтения получает один аргумент — объект записи:

(obj) => modifiedObj

Особенности поведения:

  • вызывается для каждой записи отдельно;

  • должен работать синхронно;

  • может возвращать:

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

  • применяется ко всем операциям чтения таблицы.

Если хук возвращает undefined, это не означает фильтрацию записи — объект всё равно попадёт в результат. Для фильтрации используются методы коллекций (filter, where), а не reading hook.


Базовое применение: трансформация данных

Наиболее распространённый сценарий — нормализация или денормализация данных при чтении.

Пример: добавление вычисляемого поля

db.users.hook('reading', (user) => {
    user.fullName = `${user.firstName} ${user.lastName}`;
    return user;
});

Теперь каждое прочитанное значение из таблицы users будет содержать поле fullName, хотя оно не хранится в базе.


Пример: преобразование структуры данных

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

db.orders.hook('reading', (order) => {
    return {
        ...order,
        totalPrice: order.items.reduce((sum, i) => sum + i.price * i.qty, 0)
    };
});

Такой подход позволяет хранить только сырые данные, а производные вычисления выполнять при чтении.


Использование для миграции форматов “на лету”

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

db.users.hook('reading', (user) => {
    if (!user.fullName && user.firstName && user.lastName) {
        user.fullName = `${user.firstName} ${user.lastName}`;
    }

    return user;
});

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


Декодирование и дешифрация данных

Часто reading hook используется для обратного преобразования данных, например, дешифрации:

db.secrets.hook('reading', (item) => {
    item.value = decrypt(item.value);
    return item;
});

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


Ограничения reading hook

Синхронность

Reading hook не поддерживает асинхронные операции:

// НЕЛЬЗЯ
db.users.hook('reading', async (user) => {
    user.extra = await fetchSomething();
    return user;
});

Dexie не будет ожидать Promise, и поведение станет непредсказуемым.


Нельзя использовать для фильтрации

Попытка исключать записи через return null или undefined не работает как фильтр:

db.users.hook('reading', (user) => {
    if (user.deleted) return undefined; // не фильтрует корректно
});

Фильтрация должна выполняться через:

db.users.where('deleted').equals(0)

или:

db.users.filter(u => !u.deleted)

Производительность

Так как hook вызывается для каждой записи:

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

Оптимальный подход — минимальные и дешёвые операции трансформации.


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

Если зарегистрировано несколько reading hooks на одной таблице, они выполняются последовательно в порядке добавления:

db.users.hook('reading', (u) => {
    u.step1 = true;
    return u;
});

db.users.hook('reading', (u) => {
    u.step2 = true;
    return u;
});

В результате объект последовательно проходит все трансформации.


Работа с классами (object mapping)

Reading hook может использоваться для приведения объекта к экземпляру класса:

class User {
    constructor(data) {
        Object.assign(this, data);
    }

    get isActive() {
        return this.status === 'active';
    }
}

db.users.hook('reading', (obj) => {
    return Object.setPrototypeOf(obj, User.prototype);
});

Такой подход позволяет добавлять методы и геттеры к объектам, извлекаемым из IndexedDB.


Взаимодействие с кэшем Dexie

Dexie может возвращать объекты из внутреннего кэша. Reading hook применяется и в этом случае, что приводит к важному эффекту:

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

Поэтому безопаснее возвращать новый объект:

db.users.hook('reading', (u) => {
    return { ...u, hydrated: true };
});

Особенности работы в коллекциях

Reading hook срабатывает не только при get, но и при всех операциях, возвращающих записи:

  • toArray()
  • each()
  • first()
  • last()
  • where().equals().toArray()

При этом hook вызывается после извлечения данных, но до передачи их в пользовательский код коллекции.


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

Локальная гидратация данных

db.posts.hook('reading', (post) => {
    post.createdAt = new Date(post.createdAt);
    return post;
});

Обогащение данными UI слоя

db.messages.hook('reading', (msg) => {
    msg.isMine = msg.userId === currentUserId;
    return msg;
});

Поддержка legacy схем

db.settings.hook('reading', (s) => {
    if (s.themeColor && !s.theme) {
        s.theme = s.themeColor;
    }
    return s;
});

Пограничные случаи

  • Изменение первичных ключей внутри reading hook не влияет на IndexedDB, но может ломать логику приложения.
  • Побочные эффекты (логирование, сетевые запросы) ухудшают предсказуемость и должны исключаться.
  • Мутация вложенных объектов может привести к неожиданному разделению ссылок между результатами запросов.

Архитектурная роль reading hook

Хук чтения фактически формирует слой “адаптера данных” между хранилищем IndexedDB и доменной моделью приложения. Он позволяет:

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

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