Что такое транзакция в контексте Dexie

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

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


Базовая модель транзакций IndexedDB

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

  • Атомарность — либо выполняются все операции, либо ни одна
  • Изолированность — транзакции не мешают друг другу
  • Автоматическое завершение — транзакция завершается после выхода из области видимости
  • Режим доступа — read-only или readwrite

Dexie.js сохраняет эту модель, добавляя удобные абстракции и контроль над жизненным циклом транзакции.


Создание транзакции в Dexie.js

Основной метод для работы с транзакциями — db.transaction().

Синтаксис:

db.transaction(mode, tables, callback)

Параметры:

  • mode — режим транзакции:

    • "r" или "readonly" — только чтение
    • "rw" или "readwrite" — чтение и запись
  • tables — список таблиц, участвующих в транзакции

  • callback — функция, внутри которой выполняются операции


Пример базовой транзакции

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

    await db.orders.add({
        userId: user.id,
        amount: 150
    });

    await db.users.update(user.id, { lastOrderDate: new Date() });
});

Все операции внутри выполняются атомарно. Если любая из них завершится ошибкой, изменения не будут сохранены.


Область видимости транзакции

Одной из ключевых особенностей Dexie.js является автоматическое определение области транзакции. Все обращения к таблицам внутри callback автоматически привязываются к активной транзакции.

db.transaction('rw', db.users, async () => {
    const users = await db.users.toArray();
    const count = await db.users.count();
});

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


Автоматическое завершение транзакции

IndexedDB завершает транзакцию автоматически, когда:

  • callback функции завершился без ошибок
  • все промисы внутри callback были выполнены
  • нет активных операций

В Dexie это означает, что корректная работа с async/await критически важна:

db.transaction('rw', db.items, async () => {
    const item = await db.items.get(1);
    item.checked = true;

    await db.items.put(item);
});

Если промис не был дождан (await пропущен), транзакция может завершиться раньше времени.


Откат транзакции при ошибках

Любая ошибка внутри транзакции приводит к её автоматическому откату.

db.transaction('rw', db.products, async () => {
    await db.products.add({ id: 1, name: 'A' });

    throw new Error('Ошибка');

    await db.products.add({ id: 2, name: 'B' });
});

В данном случае ни одна запись не будет сохранена.


Явное управление ошибками

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

db.transaction('rw', db.logs, async () => {
    try {
        await db.logs.add({ message: 'start' });
        throw new Error('fail');
    } catch (e) {
        await db.logs.add({ message: 'handled error' });
        throw e;
    }
});

Если ошибка повторно выбрасывается, транзакция откатывается.


Несколько таблиц в одной транзакции

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

db.transaction('rw', db.users, db.profiles, async () => {
    const id = await db.users.add({ name: 'Alex' });

    await db.profiles.add({
        userId: id,
        bio: 'Developer'
    });
});

Если добавление профиля не удаётся, пользователь также не будет создан.


Режимы транзакций

Readonly транзакции

Используются для безопасного чтения данных:

db.transaction('r', db.users, async () => {
    const users = await db.users.where('age').above(18).toArray();
});

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

  • не блокируют запись
  • быстрее выполняются
  • могут выполняться параллельно

Readwrite транзакции

Используются для операций изменения данных:

db.transaction('rw', db.users, async () => {
    await db.users.put({ id: 1, name: 'Updated' });
});

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

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

Неявные транзакции Dexie

Dexie автоматически создаёт транзакции, если операции выполняются без явного db.transaction.

await db.users.add({ name: 'Auto transaction' });

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


Проблема множества транзакций

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

for (let i = 0; i < 1000; i++) {
    await db.items.add({ value: i });
}

Это приводит к:

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

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

Группировка операций в одну транзакцию:

db.transaction('rw', db.items, async () => {
    for (let i = 0; i < 1000; i++) {
        await db.items.add({ value: i });
    }
});

Такой подход существенно ускоряет выполнение и уменьшает количество переключений контекста IndexedDB.


Поведение при конкурентных транзакциях

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

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

  • задержки выполнения
  • конфликт блокировок
  • необходимость повторных попыток

Retry-механизм транзакций

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

db.transaction('rw', db.orders, async () => {
    await db.orders.add({ id: 1 });
}).catch(err => {
    console.error('Transaction failed', err);
});

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


Вложенные транзакции

IndexedDB не поддерживает истинно вложенные транзакции. Dexie также следует этому ограничению.

Попытка открыть транзакцию внутри другой транзакции приводит к использованию уже существующей:

db.transaction('rw', db.users, async () => {
    await db.transaction('rw', db.users, async () => {
        await db.users.add({ name: 'Nested' });
    });
});

Фактически используется одна транзакция.


Контекст транзакции и Dexie.currentTransaction

Dexie хранит текущую активную транзакцию в контексте выполнения:

const tx = Dexie.currentTransaction;

Это позволяет:

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

Ограничения транзакций

Существуют важные ограничения:

  • нельзя выполнять асинхронные операции вне Dexie API внутри транзакции (fetch, setTimeout)
  • нельзя удерживать транзакцию слишком долго
  • нельзя игнорировать await
  • нельзя смешивать независимые транзакции без необходимости

Время жизни транзакции

Транзакция существует только в пределах выполнения callback. Любое отклонение от синхронного потока управления (даже через микротаски) может привести к её завершению.

db.transaction('rw', db.items, async () => {
    await db.items.add({ value: 1 });

    setTimeout(() => {
        db.items.add({ value: 2 }); // уже вне транзакции
    }, 100);
});

Вторая операция выполняется вне контекста транзакции.


Практическая значимость транзакций

Транзакции в Dexie.js обеспечивают:

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

Они являются фундаментальным механизмом, на котором строится вся модель хранения и модификации данных в библиотеке Dexie.js.