Dexie.js опирается на механизм IndexedDB, в котором изменение схемы базы данных происходит исключительно через повышение версии базы. Каждое изменение структуры — добавление таблиц, индексов, изменение ключей или трансформация данных — должно быть привязано к конкретной версии и выполняться в строго определённом порядке.
Версия базы данных в Dexie представляет собой монотонно возрастающее целое число. Любое обновление схемы требует увеличения версии:
Ключевая особенность заключается в том, что Dexie всегда выполняет миграции последовательно, начиная с текущей версии пользователя и заканчивая последней объявленной версией.
Если пользователь пропустил несколько обновлений (например, с версии 1 сразу переходит на 4), движок не выполняет только финальную миграцию. Вместо этого последовательно выполняются все промежуточные шаги:
1 → 2 → 3 → 4
Это гарантирует целостность данных при накопительных изменениях схемы.
Каждая версия задаётся через цепочку db.version(n):
const db = new Dexie("appDatabase");
db.version(1).stores({
users: "++id, name, email"
});
db.version(2).stores({
users: "++id, name, email, createdAt"
});
При таком определении Dexie фиксирует две версии схемы. При открытии базы:
Добавление .stores() определяет только структуру. Однако
реальные преобразования данных выполняются через
upgrade():
db.version(3).stores({
users: "++id, name, email, createdAt, isActive"
}).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
user.isActive = true;
});
});
Здесь происходит разделение:
.stores() — описание схемы.upgrade() — трансформация существующих данныхDexie гарантирует выполнение upgrade() внутри транзакции
соответствующей версии.
Dexie требует, чтобы версии объявлялись строго по возрастанию. Нарушение порядка приводит к ошибке и блокирует открытие базы.
Корректный порядок:
db.version(1) ...
db.version(2) ...
db.version(3) ...
Некорректные варианты:
db.version(3) ...
db.version(2) ... // ошибка
или
db.version(1) ...
db.version(3) ...
db.version(2) ... // ошибка
Когда Dexie обнаруживает, что текущая версия базы меньше последней объявленной, он формирует цепочку транзакций:
Каждая версия получает собственную транзакцию IndexedDB. Это означает:
Миграции часто требуют трансформации данных, особенно при изменении структуры индексов или ключей.
Пример добавления нового индекса с пересчётом данных:
db.version(4).stores({
users: "++id, name, email, createdAt, isActive, role"
}).upgrade(async tx => {
const users = await tx.table("users").toArray();
for (const user of users) {
if (!user.role) {
user.role = "guest";
await tx.table("users").put(user);
}
}
});
Особенность Dexie заключается в том, что upgrade-транзакция остаётся активной до завершения всех асинхронных операций. Это позволяет безопасно выполнять пакетные преобразования.
Изменение схемы индексов является одной из самых чувствительных операций. Dexie рассматривает такие изменения как структурные:
Пример:
db.version(5).stores({
users: "++id, name, email, createdAt, role, country"
});
Если ранее индекс email был уникальным, а теперь стал
обычным, IndexedDB пересоздаёт внутреннюю структуру хранилища, что
автоматически запускает upgrade-цепочку.
Dexie позволяет разделять объявление структуры и обработку данных по разным версиям:
db.version(6).stores({
users: "++id, name, email, createdAt"
});
db.version(7).stores({
users: "++id, name, email, createdAt, lastLogin"
}).upgrade(async tx => {
await tx.table("users").toCollection().modify(user => {
user.lastLogin = null;
});
});
Такой подход делает миграции читаемыми и предсказуемыми: каждая версия отвечает за одно атомарное изменение.
Если пользователь открывает приложение, где текущая база версии 2, а доступна версия 6, Dexie выполняет:
2 → 3 → 4 → 5 → 6
При этом:
Каждый upgrade() выполняется внутри транзакции,
которая:
Пример поведения при ошибке:
db.version(8).stores({
users: "++id, name, email"
}).upgrade(async tx => {
throw new Error("migration failed");
});
В этом случае:
Dexie поддерживает асинхронные операции внутри upgrade, но важно учитывать:
tx.table()Пример корректного паттерна:
db.version(9).upgrade(async tx => {
const table = tx.table("users");
await table.toCollection().modify(user => {
user.normalized = user.name.toLowerCase();
});
});
При больших объёмах данных миграции могут быть оптимизированы через пакетную обработку:
db.version(10).upgrade(async tx => {
const chunkSize = 500;
let collection = tx.table("users").toCollection();
let lastId = null;
while (true) {
const batch = await collection
.filter(u => !lastId || u.id > lastId)
.limit(chunkSize)
.toArray();
if (batch.length === 0) break;
for (const user of batch) {
user.flagged = false;
await tx.table("users").put(user);
lastId = user.id;
}
}
});
Такой подход снижает нагрузку на память и уменьшает риск блокировки UI.
Dexie сохраняет возможность чтения старых схем до момента завершения миграции. Это означает:
Это поведение позволяет безопасно трансформировать данные без потери информации.
Наиболее распространённые проблемы:
В реальных приложениях цепочка миграций может достигать десятков версий. Для поддержания устойчивости применяются подходы:
Такой подход сохраняет предсказуемость поведения IndexedDB и Dexie при любых сценариях обновления клиента.