Явные транзакции через db.transaction()

Сущность транзакций в Dexie.js

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

В основе Dexie.js лежит нативный IndexedDB API, где транзакции являются обязательным механизмом доступа к данным. Dexie упрощает их использование, предоставляя высокоуровневый интерфейс db.transaction(), который делает управление потоками данных более предсказуемым и структурированным.

Ключевые свойства транзакций:

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

Синтаксис db.transaction()

Базовая форма объявления транзакции в Dexie.js:

db.transaction(mode, tables, callback)

Параметры:

  • mode — режим доступа:

    • "r" (read)
    • "rw" (readwrite)
  • tables — список таблиц (stores), участвующих в транзакции

  • callback — функция, содержащая операции с базой

Пример:

await db.transaction("rw", db.users, db.orders, async () => {
    await db.users.add({ id: 1, name: "Alex" });
    await db.orders.add({ id: 100, userId: 1, total: 250 });
});

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

Только чтение ("r")

Используется для безопасного получения данных без модификации состояния базы.

await db.transaction("r", db.users, async () => {
    const allUsers = await db.users.toArray();
    console.log(allUsers);
});

Особенности:

  • позволяет параллельные транзакции чтения;
  • не блокирует запись;
  • быстрее по сравнению с readwrite.

Чтение и запись ("rw")

Используется при необходимости изменения данных.

await db.transaction("rw", db.users, async () => {
    const user = await db.users.get(1);
    user.lastLogin = Date.now();
    await db.users.put(user);
});

Особенности:

  • блокирует соответствующие object stores;
  • гарантирует атомарность изменений;
  • используется для критических операций.

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

Все обращения к IndexedDB внутри db.transaction() автоматически привязываются к текущей транзакции, если они происходят в рамках её асинхронного контекста.

await db.transaction("rw", db.users, async () => {
    const u1 = await db.users.get(1);
    const u2 = await db.users.get(2);

    await db.users.put({ ...u1, active: true });
    await db.users.put({ ...u2, active: true });
});

Важно:

  • нельзя использовать внешние соединения к store вне транзакции;
  • все операции должны быть выполнены внутри callback.

Автоматический rollback

Dexie.js автоматически откатывает транзакцию при возникновении исключения.

await db.transaction("rw", db.users, async () => {
    await db.users.add({ id: 1, name: "John" });

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

    await db.users.add({ id: 2, name: "Jane" });
});

Результат:

  • ни одна запись не сохраняется;
  • база остаётся в прежнем состоянии.

Множественные object stores в одной транзакции

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

await db.transaction("rw", db.users, db.logs, db.sessions, async () => {
    const user = await db.users.get(1);

    await db.sessions.add({ userId: user.id, started: Date.now() });
    await db.logs.add({ message: "User login", userId: user.id });
});

Преимущества:

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

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

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

await db.transaction("rw", db.users, async () => {
    await updateUser(1);
});

async function updateUser(id) {
    const user = await db.users.get(id);
    user.visits++;
    await db.users.put(user);
}

Особенность:

  • функция updateUser автоматически использует уже активную транзакцию;
  • нет необходимости повторно объявлять db.transaction().

Ошибки и отмена транзакции

Любая ошибка внутри callback приводит к отмене всей транзакции:

await db.transaction("rw", db.users, async () => {
    await db.users.add({ id: 1 });

    // Ошибка ключа
    await db.users.add({ id: 1 });
});

Поведение:

  • первая операция не фиксируется;
  • транзакция завершается с reject;
  • IndexedDB выполняет rollback автоматически.

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

Dexie.js завершает транзакцию автоматически при успешном выходе из callback.

Однако важно учитывать:

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

Пример корректного завершения:

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

Использование Promise.all внутри транзакции

Параллельные операции допустимы, если они относятся к одной транзакции.

await db.transaction("rw", db.users, async () => {
    await Promise.all([
        db.users.put({ id: 1, name: "A" }),
        db.users.put({ id: 2, name: "B" })
    ]);
});

Особенности:

  • операции выполняются конкурентно;
  • транзакция остаётся активной до завершения всех промисов;
  • ошибки любой из операций приводят к rollback.

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

IndexedDB накладывает ряд ограничений, которые сохраняются в Dexie.js:

  • нельзя использовать разные базы в одной транзакции;
  • нельзя удерживать транзакцию слишком долго (браузер может её прервать);
  • нельзя выполнять DOM-операции внутри транзакции;
  • нельзя открывать новые транзакции внутри callback в другом объекте базы.

Поведение при долгих операциях

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

await db.transaction("rw", db.users, async () => {
    await db.users.get(1);

    await new Promise(resolve => setTimeout(resolve, 5000));

    await db.users.put({ id: 1, name: "Late update" });
});

Риски:

  • возможный TransactionInactiveError;
  • потеря атомарности;
  • непредсказуемое завершение.

Оптимальная структура транзакций

Эффективное использование db.transaction() предполагает минимизацию времени жизни транзакции и включение только необходимых операций.

Пример корректной структуры:

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

    await db.users.put({ ...user, active: true });
    await db.logs.add({ userId: user.id, event: "activate" });
});

Принципы:

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

Поведение чтения внутри readwrite-транзакции

Транзакция "rw" включает возможность чтения, поэтому дополнительные "r" не требуются.

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

Особенности:

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

Согласованность данных между store

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

await db.transaction("rw", db.accounts, db.transactions, async () => {
    const acc = await db.accounts.get(1);

    acc.balance -= 100;
    await db.accounts.put(acc);

    await db.transactions.add({
        accountId: acc.id,
        amount: -100
    });
});

Результат:

  • баланс и транзакция изменяются одновременно;
  • невозможна ситуация частичного обновления.

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

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

  • read-only транзакции могут выполняться параллельно;
  • readwrite транзакции блокируют store;
  • Dexie управляет ожиданием доступа автоматически.

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

Транзакция привязана к текущему асинхронному контексту. Потеря контекста приводит к ошибкам:

let t;

await db.transaction("rw", db.users, async tx => {
    t = tx;
});

await db.users.get(1); // вне транзакции

Особенность:

  • tx нельзя использовать вне callback;
  • операции вне транзакции не наследуют контекст.

Роль db.transaction() в архитектуре Dexie.js

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

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

Использование db.transaction() формирует предсказуемую модель работы с IndexedDB, где каждая операция имеет строго определённые границы выполнения и гарантии согласованности данных.