Dexie.js строится вокруг явного описания структуры IndexedDB через
схему базы данных, где каждая таблица и её индексы фиксируются на этапе
инициализации. Однако в реальных приложениях прямое взаимодействие со
схемой на уровне version().stores() часто становится узким
местом: схема начинает дублироваться в коде, миграции разрастаются, а
доменная модель смешивается с деталями хранения данных. Слой абстракции
над схемой решает эту проблему, отделяя описание структуры данных от
логики приложения и предоставляя единый центр управления
изменениями.
В стандартной модели Dexie схема описывается через версии:
const db = new Dexie("AppDatabase");
db.version(1).stores({
users: "++id,name,email",
orders: "++id,userId,createdAt"
});
Каждая версия фиксирует состояние таблиц на момент миграции. Любое изменение структуры требует новой версии:
db.version(2).stores({
users: "++id,name,email,role",
orders: "++id,userId,createdAt,status"
});
Такая модель работает корректно, но имеет ряд ограничений:
Слой абстракции над схемой вводит промежуточное представление структуры базы данных, независимое от версий Dexie. В этом подходе схема существует как единый объект, а версии лишь интерпретируют изменения этого объекта.
Ключевая идея заключается в разделении:
Логическая схема описывает данные в структурированном виде:
const schemaModel = {
users: {
key: "id",
autoIncrement: true,
fields: {
name: { index: true },
email: { index: true, unique: true },
role: { index: false }
}
},
orders: {
key: "id",
autoIncrement: true,
fields: {
userId: { index: true },
createdAt: { index: true },
status: { index: true }
}
}
};
Такое представление не зависит от Dexie и не использует строковую нотацию индексов. Оно становится источником истины для всей структуры данных.
Dexie требует строкового описания индексов. Поэтому слой абстракции включает функцию компиляции:
function compileStoreSchema(model) {
const stores = {};
for (const [tableName, definition] of Object.entries(model)) {
const indexes = [];
if (definition.key && definition.autoIncrement) {
indexes.push("++" + definition.key);
} else if (definition.key) {
indexes.push(definition.key);
}
for (const [field, config] of Object.entries(definition.fields)) {
if (config.index && field !== definition.key) {
indexes.push(field);
}
}
stores[tableName] = indexes.join(",");
}
return stores;
}
Результатом становится объект, пригодный для Dexie:
const stores = compileStoreSchema(schemaModel);
db.version(1).stores(stores);
Одним из ключевых элементов абстракции является вычисление различий между версиями схемы. Вместо ручного описания каждой миграции формируется дифф между двумя состояниями модели.
Сравнение может строиться на уровне:
Пример функции сравнения:
function diffSchemas(prev, next) {
const changes = {
addedTables: [],
removedTables: [],
modifiedTables: []
};
for (const table of Object.keys(next)) {
if (!prev[table]) {
changes.addedTables.push(table);
}
}
for (const table of Object.keys(prev)) {
if (!next[table]) {
changes.removedTables.push(table);
}
}
for (const table of Object.keys(next)) {
if (prev[table] && JSON.stringify(prev[table]) !== JSON.stringify(next[table])) {
changes.modifiedTables.push(table);
}
}
return changes;
}
На основе этого диффа формируется набор версий Dexie, каждая из которых отражает конкретное изменение схемы.
Dexie требует последовательного описания всех версий. Абстрактный слой может генерировать их автоматически:
function buildDexieVersions(db, schemaHistory) {
schemaHistory.forEach((schema, index) => {
const versionNumber = index + 1;
const stores = compileStoreSchema(schema);
db.version(versionNumber).stores(stores);
});
}
История схемы становится массивом состояний:
const schemaHistory = [
schemaV1,
schemaV2,
schemaV3
];
Такой подход исключает ручное управление версиями и уменьшает риск рассинхронизации структуры.
Абстракция схемы позволяет связать таблицы Dexie с доменными сущностями без привязки к строковой нотации индексов.
Пример доменной модели:
class User {
constructor({ id, name, email, role }) {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
}
}
Маппинг слоя хранения:
function toUserModel(row) {
return new User(row);
}
function toUserRow(user) {
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role
};
}
Таким образом схема базы данных перестаёт проникать в бизнес-логику.
Встроенная строковая система Dexie ("++id,name,email")
компактна, но плохо масштабируется. Абстракция позволяет выразить
индексы декларативно и расширяемо:
Пример расширенного описания:
fields: {
email: { index: true, unique: true },
fullName: { index: true, compound: ["firstName", "lastName"] }
}
Компилятор схемы преобразует это в корректную строку Dexie, скрывая сложность преобразования.
Абстракция схемы может рассматривать изменения как последовательность событий:
Пример представления:
const schemaEvents = [
{ type: "ADD_TABLE", table: "users" },
{ type: "ADD_FIELD", table: "users", field: "email", index: true },
{ type: "ADD_INDEX", table: "orders", field: "createdAt" }
];
Эти события затем применяются к базовой модели, формируя новую версию схемы. Такой подход делает структуру базы воспроизводимой и детерминированной.
Dexie поддерживает миграции через метод upgrade(),
который может использоваться совместно с абстрактным слоем:
db.version(2).stores(compileStoreSchema(schemaV2)).upgrade(tx => {
return tx.table("users").toCollection().modify(user => {
if (!user.role) {
user.role = "user";
}
});
});
Абстракция позволяет генерировать такие миграции автоматически на основе различий схемы, связывая структурные изменения с трансформацией данных.
Логическая схема становится переносимым артефактом. Она может использоваться:
Dexie-слой при этом остаётся тонкой адаптацией, а не источником истины.
Схема может быть разделена на модули и собрана в единую структуру:
const userSchema = { ... };
const orderSchema = { ... };
const schemaModel = {
...userSchema,
...orderSchema
};
Это позволяет строить масштабируемые базы данных, где каждый модуль отвечает за свою часть структуры, не вмешиваясь в глобальное описание.
Абстракция устраняет прямую зависимость от строковой DSL Dexie. В коде приложения больше не присутствуют конструкции вида:
"++id,name,email"
Вместо этого используется структурированное описание, которое:
Dexie остаётся только движком выполнения, а не источником описания структуры данных.