Миграции схемы в Dexie.js выполняются через механизм версий базы
данных, где каждое изменение структуры описывается отдельным блоком
version().stores() и сопровождается опциональной функцией
upgrade(). При этом процесс обновления схемы всегда
асинхронный и потенциально прерываемый, поэтому обработка ошибок
становится критическим элементом архитектуры хранилища.
В основе Dexie.js лежит IndexedDB, и большинство ошибок миграции связаны не с самой библиотекой, а с ограничениями движка:
Миграции выполняются внутри транзакции версии базы, и любая ошибка приводит к откату текущего шага upgrade. Однако откат не всегда восстанавливает предыдущую структуру в привычном смысле — изменения схемы уже частично применены на уровне IndexedDB.
Каждая миграция в Dexie.js задаётся функцией:
db.version(2).stores({
users: "++id,email,name"
}).upgrade(tx => {
return tx.users.toCollection().modify(user => {
user.name = user.name || "unknown";
});
});
Ошибка внутри modify прерывает всю транзакцию версии 2.
Поэтому критически важно учитывать атомарность: любая ошибка = откат
всей миграции.
db.version(2).stores({
users: "++id,email,name"
}).upgrade(async tx => {
try {
await tx.users.toCollection().modify(user => {
if (!user.email) {
throw new Error("Invalid user: missing email");
}
});
} catch (err) {
console.error("Migration failed:", err);
throw err;
}
});
Повторное пробрасывание ошибки обязательно: подавление приведёт к неконсистентной схеме.
Практика миграций в Dexie.js предполагает минимизацию риска за счёт дробления изменений:
Пример безопасного подхода:
db.version(2).stores({
users: "++id,email,name_v2"
}).upgrade(async tx => {
await tx.users.toCollection().modify(user => {
user.name_v2 = user.name || null;
});
});
db.version(3).stores({
users: "++id,email,name_v2"
}).upgrade(async tx => {
await tx.users.toCollection().modify(user => {
delete user.name;
});
});
Разделение снижает вероятность частичного повреждения данных.
Ошибки миграции часто проявляются не внутри upgrade, а
на этапе открытия:
try {
await db.open();
} catch (err) {
console.error("DB open failed:", err);
}
Типичные причины:
Dexie предоставляет событие блокировки:
db.on("blocked", () => {
console.warn("Upgrade blocked by another open connection");
});
И событие изменения версии:
db.on("versionchange", () => {
db.close();
});
Эти события критичны для предотвращения гонок при миграциях.
В реальных приложениях Dexie.js миграции должны учитывать частичное выполнение и повторные попытки.
Миграция должна быть безопасной при повторном запуске:
db.version(2).stores({
users: "++id,email,name"
}).upgrade(async tx => {
await tx.users.toCollection().modify(user => {
if (!("migrated" in user)) {
user.name = user.name || "unknown";
user.migrated = true;
}
});
});
Флаг migrated защищает от повторной трансформации.
Ошибки часто возникают при работе с реальными данными:
Защита через дефолтные значения:
await tx.users.toCollection().modify(user => {
user.age = Number.isFinite(user.age) ? user.age : 0;
user.tags = Array.isArray(user.tags) ? user.tags : [];
});
При необходимости можно изолировать проблемные записи:
await tx.users.toCollection().modify(user => {
try {
user.profile = JSON.parse(user.profileRaw);
} catch {
user.profile = null;
}
});
Изменение stores() в Dexie.js может приводить к
критическим сбоям:
Типовой подход:
db.version(2).stores({
users: "++id,email,username"
});
Если поле переименовывается, безопаснее сохранить старое поле временно:
db.version(3).stores({
users: "++id,email,username,new_username"
}).upgrade(async tx => {
await tx.users.toCollection().modify(user => {
user.new_username = user.username;
});
});
При больших объёмах данных ошибки часто связаны с:
Рекомендуется пакетная обработка:
const batchSize = 500;
let lastId = 0;
while (true) {
const batch = await tx.users
.where("id")
.above(lastId)
.limit(batchSize)
.toArray();
if (batch.length === 0) break;
for (const user of batch) {
user.flag = true;
await tx.users.put(user);
lastId = user.id;
}
}
Такой подход снижает вероятность AbortError.
Для устойчивости миграций в Dexie.js важно фиксировать состояние:
db.on("error", (err) => {
console.error("Global DB error:", err);
});
Дополнительно полезно логировать версии:
console.log("DB version:", db.verno);
И текущую схему:
console.log(db.tables.map(t => t.name));
IndexedDB не поддерживает полноценные rollback-транзакции схемы. Это означает:
Поэтому каждая миграция должна быть:
При открытии нескольких вкладок возможна гонка версий:
db.on("blocked", () => {
db.close();
});
Дополнительно:
window.addEventListener("beforeunload", () => {
db.close();
});
Это снижает вероятность состояния, когда одна вкладка держит старую версию, а другая пытается выполнить upgrade.
Некоторые ошибки невозможно обработать внутри миграции:
try {
await db.open();
} catch (err) {
if (err.name === "VersionError") {
console.error("Incompatible DB version");
} else {
console.error("Unexpected DB failure", err);
}
}
При повреждённой базе часто требуется пересоздание:
await db.delete();
await db.open();
Этот сценарий применяется как крайняя мера восстановления.
После успешного upgrade полезно выполнять проверку:
await db.transaction("r", db.users, async () => {
const count = await db.users.count();
if (count < 0) throw new Error("Corrupted state");
});
Такие проверки позволяют обнаружить логические ошибки миграции до начала работы приложения.
Миграции в Dexie.js строятся вокруг нескольких принципов:
Каждая ошибка в процессе миграции рассматривается как сигнал к либо повторному выполнению безопасной операции, либо к остановке с контролируемым переходом в состояние восстановления данных.