Автоматические транзакции в одиночных операциях

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

Автоматическая транзакция создаётся для каждого вызова методов уровня таблицы:

  • get()
  • put()
  • add()
  • update()
  • delete()
  • toArray()
  • count()
  • each()

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


Принцип изоляции одиночных операций

При выполнении одиночной операции Dexie формирует транзакцию, содержащую только одну таблицу (или набор таблиц, если это требуется внутренней реализацией индексов). Это означает:

  • транзакция существует только на время выполнения запроса;
  • автоматически завершается после завершения Promise-цепочки;
  • не требует ручного управления transaction().

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

Пример логики:

db.users.get(1)

Фактически превращается в:

  • создание readonly транзакции
  • выполнение запроса
  • автоматический commit после получения результата

Режимы автоматических транзакций

Dexie выбирает режим транзакции на основе операции:

Readonly транзакции:

  • get
  • toArray
  • count
  • where().first()

Readwrite транзакции:

  • put
  • add
  • update
  • delete

Это разделение важно, поскольку IndexedDB строго различает режимы доступа. Попытка записи в readonly транзакции приводит к ошибке уровня движка.

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


Жизненный цикл автоматической транзакции

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

  1. Инициализация

    • создание транзакции IndexedDB
    • привязка к нужным объектным хранилищам (object stores)
    • установка режима доступа
  2. Выполнение операции

    • запуск запроса (request)
    • регистрация обработчиков успеха и ошибки
    • привязка к Promise Dexie
  3. Завершение

    • commit транзакции после завершения всех микрозадач
    • освобождение ресурсов
  4. Откат (при ошибке)

    • автоматический abort транзакции
    • отмена всех операций внутри текущего контекста

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


Связь с Promise-моделью Dexie

Dexie тесно связывает IndexedDB транзакции с Promise-цепочками. Это означает, что транзакция живёт до тех пор, пока не завершится асинхронная цепочка выполнения.

db.users.get(1).then(user => {
    return db.orders.where('userId').equals(user.id).toArray();
});

В этом случае:

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

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


Ограниченность области действия

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

Пример:

db.users.get(1)
  .then(user => db.orders.where('userId').equals(user.id).toArray());

Здесь создаются две независимые транзакции:

  • одна для get
  • другая для where().toArray()

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


Отличие от явных транзакций

Dexie предоставляет явные транзакции через db.transaction():

db.transaction('rw', db.users, db.orders, async () => {
    const user = await db.users.get(1);
    await db.orders.add({ userId: user.id });
});

Различие принципиальное:

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

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


Поведение при параллельных вызовах

При одновременном выполнении нескольких операций Dexie создаёт отдельные транзакции для каждой:

db.users.get(1);
db.users.put({ id: 2, name: 'A' });
db.users.delete(3);

Каждый вызов:

  • имеет собственный контекст транзакции
  • не блокирует другие операции (если IndexedDB позволяет параллельность)
  • завершается независимо

Это улучшает производительность, но исключает атомарность между вызовами.


Оптимизация через автоматические транзакции

Dexie использует ряд оптимизаций при автоматическом создании транзакций:

  • reusing store references внутри одного запроса
  • минимизация времени жизни транзакции
  • группировка внутренних операций чтения
  • lazy commit после завершения Promise

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


Ошибки и их распространение

Ошибки в автоматических транзакциях имеют локальный характер:

  • ошибка внутри get() завершает только эту транзакцию
  • не влияет на другие независимые операции
  • приводит к rejection Promise

Пример:

db.users.add({ id: 1 }).catch(err => console.error(err));

Если операция нарушает constraint (например, дубликат ключа), транзакция откатывается полностью, но только в рамках этой операции.


Внутренние особенности Dexie

Dexie управляет автоматическими транзакциями через внутренний слой обёрток над IndexedDB:

  • Dexie.currentTransaction отслеживает активный контекст
  • операции регистрируются в текущем scope
  • при отсутствии явного transaction scope создаётся временный

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


Консистентность данных

Автоматические транзакции гарантируют:

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

Однако они не гарантируют согласованность между несколькими операциями, выполненными последовательно.

Это особенно важно при работе с:

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

В таких случаях требуется явная транзакция.


Поведение при сложных запросах

Запросы с индексами и фильтрацией:

db.orders.where('status').equals('pending').toArray();

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

Dexie:

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

Значение автоматических транзакций в архитектуре Dexie

Автоматические транзакции формируют базовый уровень абстракции библиотеки. Они позволяют:

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

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