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

Модель транзакций в IndexedDB и роль Dexie.js

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

В Dexie.js транзакции представлены двумя основными режимами:

  • readonly — транзакции только для чтения
  • readwrite — транзакции для чтения и записи

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


Основные характеристики транзакций Dexie.js

Атомарность операций

Транзакция рассматривается как единый блок работы:

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

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

Область видимости

Транзакция в Dexie.js всегда привязана к набору таблиц:

db.transaction('rw', db.users, db.orders, async () => {
    // доступ к users и orders
});

Если внутри транзакции происходит обращение к таблице, не включённой в список, возникает ошибка.


Режим readonly

Семантика режима

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

Характерные свойства:

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

Поведение в IndexedDB

Внутри IndexedDB readonly-транзакции:

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

Dexie.js использует этот режим по умолчанию для всех операций чтения вне явно заданной транзакции.


Примеры использования readonly

Простое чтение данных

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

Поиск по индексу

db.transaction('readonly', db.orders, async () => {
    const pending = await db.orders
        .where('status')
        .equals('pending')
        .toArray();
});

Оптимизационные особенности readonly

Readonly транзакции позволяют IndexedDB:

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

Особенно это заметно при массовых запросах:

await db.transaction('r', db.products, db.categories, async () => {
    const products = await db.products.toArray();
    const categories = await db.categories.toArray();
});

Режим readwrite

Семантика режима

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

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

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


Поведение блокировок

В режиме readwrite IndexedDB накладывает строгие ограничения:

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

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


Пример базовой записи

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

Комбинированные операции

Readwrite транзакции позволяют выполнять сложные сценарии:

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

    await db.inventory.where('productId')
        .equals(order.productId)
        .modify(item => {
            item.stock -= order.quantity;
        });

    await db.orders.update(1, { status: 'processed' });
});

Отличия readonly и readwrite

1. Уровень блокировок

  • readonly: не блокирует запись
  • readwrite: блокирует соответствующие хранилища

2. Возможность изменения данных

  • readonly: строго запрещена
  • readwrite: разрешена

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

Readonly транзакции обычно быстрее, так как:

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

Readwrite транзакции медленнее из-за:

  • необходимости ожидания блокировок
  • контроля целостности данных

4. Конкурентность

// могут выполняться параллельно
db.transaction('r', db.users, async () => {});
db.transaction('r', db.users, async () => {});
// могут конфликтовать
db.transaction('rw', db.users, async () => {});
db.transaction('rw', db.users, async () => {});

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

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

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

await db.users.toArray();
await db.users.get(1);

Любые операции чтения выполняются в скрытой readonly транзакции.


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

await db.users.add({ name: 'Maria' });
await db.users.put({ id: 1, name: 'Updated' });
await db.users.delete(3);

Каждая операция записи запускается в отдельной readwrite транзакции, если не объединена вручную.


Явные транзакции и их преимущества

Явное управление транзакциями позволяет:

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

Пример группировки операций

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

    await db.orders.add({
        userId: user.id,
        total: 250
    });

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

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


Поведение при ошибках

Abort транзакции

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

db.transaction('rw', db.users, async () => {
    await db.users.add({ id: 1 });
    throw new Error('failure');
});

Результат:

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

Ошибки в readonly

Readonly транзакции не допускают изменений, поэтому ошибки возникают при попытке записи:

db.transaction('r', db.users, async () => {
    await db.users.add({ name: 'Invalid' }); // ошибка
});

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

Dexie.js не поддерживает полноценные вложенные транзакции как независимые сущности. Вместо этого:

  • используется общий контекст транзакции
  • все операции объединяются в одну транзакцию уровня Dexie

Поведение внутри цепочек промисов

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

    await Promise.resolve().then(() =>
        db.users.add({ name: 'B' })
    );
});

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


Автокоммит транзакций

Транзакции в Dexie.js завершаются автоматически:

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

Readwrite транзакция удерживается активной до завершения всех await-операций внутри её области.


Типичные ошибки при работе с режимами

1. Попытка записи в readonly

Наиболее частая ошибка:

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

2. Обращение к неуказанной таблице

db.transaction('rw', db.users, async () => {
    await db.orders.add({}); // ошибка
});

3. Разделение связанных операций

await db.users.add({ name: 'A' });
await db.orders.add({ userId: 1 });

Проблема: отсутствие атомарности между операциями.


Рекомендации по выбору режима

Readonly используется для:

  • выборок данных
  • фильтрации
  • агрегации
  • построения UI-состояния

Readwrite используется для:

  • изменения состояния базы
  • бизнес-операций
  • синхронизации данных
  • пакетных обновлений

Влияние режима на архитектуру приложения

Разделение на readonly и readwrite транзакции формирует структуру взаимодействия с данными:

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

Такое разделение снижает вероятность гонок данных и повышает предсказуемость состояния IndexedDB.