Переименование полей и преобразование данных в Dexie.js играет ключевую роль при эволюции структуры IndexedDB-базы без потери совместимости и пользовательских данных. В отличие от традиционных SQL-баз, IndexedDB не предоставляет встроенных механизмов миграции схемы, поэтому любые изменения структуры данных — включая изменение названий полей или формата хранения — реализуются на уровне приложения через версионирование базы и миграционные скрипты.
Dexie.js строит работу со схемой базы поверх механизма версий. Каждая
новая версия базы может описывать новую структуру таблиц через
version(n).stores(). Однако изменение схемы не затрагивает
существующие данные автоматически.
Ключевая особенность:
Dexie не модифицирует записи при изменении схемы — разработчик обязан явно описать миграцию.
Это означает, что переименование поля или изменение формата данных
требует дополнительного шага трансформации, обычно через
upgrade().
В IndexedDB данные хранятся как JavaScript-объекты, сериализованные
по ключам. Если поле firstName было переименовано в
name, база не понимает семантики этого изменения.
Пример исходной схемы:
db.version(1).stores({
users: "++id, firstName, lastName"
});
Обновлённая версия:
db.version(2).stores({
users: "++id, name, lastName"
});
После такого изменения:
firstName остаётся в старых записяхname отсутствует в старых данныхБез миграции возникает смешение форматов.
Механизм upgrade() позволяет выполнять произвольный
JavaScript-код при обновлении версии базы. Это основной инструмент для
переименования полей.
db.version(2).stores({
users: "++id, name, lastName"
}).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
user.name = user.firstName;
delete user.firstName;
});
});
Здесь происходит:
usersnamefirstNameВажно: операция выполняется внутри транзакции, что обеспечивает атомарность миграции.
Метод modify() работает на уровне коллекции и позволяет
изменять каждую запись без её полного извлечения и повторной записи
вручную.
Поведение:
put() для каждой записиПример переименования нескольких полей:
db.version(3).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
user.fullName = `${user.name} ${user.lastName}`;
delete user.name;
delete user.lastName;
});
});
Помимо переименования, часто требуется изменение структуры данных. Это может включать:
Исходная версия хранит дату как строку:
createdAt: "2025-01-10"
Новая версия требует Date:
db.version(2).upgrade(tx => {
return tx.table("orders").toCollection().modify(order => {
order.createdAt = new Date(order.createdAt);
});
});
Иногда данные требуют реорганизации структуры объекта.
Исходная модель:
{
id: 1,
city: "Almaty",
street: "Abay",
house: "10"
}
Новая модель:
{
id: 1,
address: {
city: "Almaty",
street: "Abay",
house: "10"
}
}
Миграция:
db.version(2).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
user.address = {
city: user.city,
street: user.street,
house: user.house
};
delete user.city;
delete user.street;
delete user.house;
});
});
Dexie выполняет версии строго последовательно. Если существует цепочка:
и пользователь открывает базу версии 3 впервые, будут выполнены все миграции по порядку.
Это позволяет строить накопительные преобразования:
db.version(2).upgrade(...)
db.version(3).upgrade(...)
db.version(4).upgrade(...)
Каждый шаг работает только с изменениями предыдущей версии.
При работе с переименованием и преобразованием следует учитывать ограничения IndexedDB:
Миграции выполняются в браузере и могут быть прерваны при закрытии вкладки.
Большие коллекции могут вызывать блокировку UI при синхронной
обработке через modify().
Каждая версия мигрирует данные отдельно, но не существует глобальной транзакции между версиями.
Для больших таблиц часто используется поэтапная миграция через курсоры:
db.version(2).upgrade(async tx => {
const table = tx.table("logs");
await table.toCollection().modify(log => {
if (log.level === "warn") {
log.level = "warning";
}
});
});
В более сложных случаях можно обрабатывать данные порционно:
db.version(3).upgrade(async tx => {
const chunkSize = 100;
const table = tx.table("events");
let lastId = 0;
while (true) {
const chunk = await table
.where("id")
.above(lastId)
.limit(chunkSize)
.toArray();
if (chunk.length === 0) break;
await Promise.all(chunk.map(item => {
item.timestamp = Date.parse(item.timestamp);
return table.put(item);
}));
lastId = chunk[chunk.length - 1].id;
}
});
В переходный период часто требуется поддержка двух форматов одновременно. Это позволяет избежать сбоев в приложении при частичном обновлении данных.
function getUserName(user) {
return user.name || user.firstName;
}
Такой подход используется совместно с миграцией, если база может содержать смешанные версии записей.
Удаление полей следует выполнять только после полной миграции данных.
Типичный шаблон:
db.version(3).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
if (user.firstName && !user.name) {
user.name = user.firstName;
}
});
});
db.version(4).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
delete user.firstName;
});
});
При изменении структуры данных необходимо учитывать обновление
индексов. Если поле участвует в индексе, его изменение требует
пересоздания схемы через stores().
Пример:
db.version(2).stores({
users: "++id, name, age"
});
Если ранее индекс был:
firstName
его замена на name означает:
Наиболее безопасная стратегия состоит из трёх шагов:
Это снижает риск потери данных и упрощает откат изменений на уровне версий IndexedDB.