Транзакция в Dexie.js представляет собой механизм атомарного выполнения операций над данными в IndexedDB, при котором набор действий либо полностью завершается успешно, либо полностью откатывается при ошибке. Такой подход обеспечивает согласованность состояния базы данных и защищает от частично выполненных операций, которые могут привести к неконсистентным данным.
В основе Dexie.js лежит IndexedDB, а транзакции являются ключевым элементом её модели конкурентного доступа. Dexie лишь упрощает работу с этим механизмом, предоставляя более высокоуровневый и предсказуемый API.
IndexedDB использует транзакционную модель, в которой каждая операция чтения или записи выполняется внутри транзакции. Основные свойства:
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 завершает транзакцию автоматически, когда:
В 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'
});
});
Если добавление профиля не удаётся, пользователь также не будет создан.
Используются для безопасного чтения данных:
db.transaction('r', db.users, async () => {
const users = await db.users.where('age').above(18).toArray();
});
Readonly транзакции:
Используются для операций изменения данных:
db.transaction('rw', db.users, async () => {
await db.users.put({ id: 1, name: 'Updated' });
});
Readwrite транзакции:
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 автоматически управляет очередями транзакций, но возможны ситуации:
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 хранит текущую активную транзакцию в контексте выполнения:
const tx = Dexie.currentTransaction;
Это позволяет:
Существуют важные ограничения:
awaitТранзакция существует только в пределах выполнения callback. Любое отклонение от синхронного потока управления (даже через микротаски) может привести к её завершению.
db.transaction('rw', db.items, async () => {
await db.items.add({ value: 1 });
setTimeout(() => {
db.items.add({ value: 2 }); // уже вне транзакции
}, 100);
});
Вторая операция выполняется вне контекста транзакции.
Транзакции в Dexie.js обеспечивают:
Они являются фундаментальным механизмом, на котором строится вся модель хранения и модификации данных в библиотеке Dexie.js.