Разделение схемы на модули

Разделение схемы базы данных в Dexie.js на модули опирается на идею изоляции доменных областей, при которой каждая функциональная часть приложения описывает собственные таблицы, индексы и миграции независимо от остальных. Такой подход особенно важен при росте приложения, когда монолитная схема становится источником конфликтов версий, дублирования индексов и сложностей сопровождения.

Монолитное описание схемы в одном месте приводит к нескольким типичным ограничениям:

  • рост файла и ухудшение читаемости по мере добавления таблиц;
  • конфликты при параллельной разработке разных модулей;
  • невозможность локальной эволюции доменных частей без риска затронуть другие;
  • усложнение миграций, когда изменения одного домена требуют пересборки всей схемы;
  • зависимость бизнес-логики от глобального состояния схемы.

При использовании Dexie.js эти проблемы проявляются особенно заметно, поскольку схема определяется через версионирование базы и полностью пересоздаётся на этапе upgrade.

Принцип модульного описания схемы

Модульный подход предполагает разбиение схемы на независимые фрагменты, каждый из которых отвечает за собственную предметную область:

  • модуль пользователей;
  • модуль заказов;
  • модуль логирования;
  • модуль настроек.

Каждый модуль экспортирует описание таблиц и индексных полей, а также при необходимости — собственные миграции. Далее эти части агрегируются в единую конфигурацию базы.

Ключевая идея заключается в том, что схема перестаёт быть централизованной сущностью и становится композируемой.

Базовая структура модулей схемы

Типовая структура проекта с модульной схемой выглядит следующим образом:

db/
  core/
    baseDb.ts
  modules/
    users/
      schema.ts
      migrations.ts
    orders/
      schema.ts
      migrations.ts
    logs/
      schema.ts
      migrations.ts
  composeSchema.ts

Каждый модуль содержит минимум два слоя ответственности:

  • описание таблиц (stores);
  • миграционные преобразования данных.

Описание схемы в модуле

Каждый модуль определяет только свою часть хранилища:

// db/modules/users/schema.ts

export const usersSchema = {
  users: "++id, email, createdAt, role"
};
// db/modules/orders/schema.ts

export const ordersSchema = {
  orders: "++id, userId, status, createdAt",
  orderItems: "++id, orderId, productId"
};

Такое разделение позволяет локализовать изменения структуры без необходимости редактирования единого файла схемы.

Композиция схем

Объединение модулей выполняется через простое слияние объектов:

// db/composeSchema.ts

import { usersSchema } from "./modules/users/schema";
import { ordersSchema } from "./modules/orders/schema";
import { logsSchema } from "./modules/logs/schema";

export const fullSchema = {
  ...usersSchema,
  ...ordersSchema,
  ...logsSchema
};

На уровне Dexie.js итоговая схема передаётся в version().stores().

import Dexie from "dexie";
import { fullSchema } from "./composeSchema";

export class AppDatabase extends Dexie {
  constructor() {
    super("app_db");

    this.version(1).stores(fullSchema);
  }
}

Такой подход сохраняет единый источник правды для runtime, при этом оставляя разработку модульной.

Изоляция миграций по модулям

Миграции являются наиболее чувствительной частью схемы. При модульном подходе они также разделяются:

// db/modules/users/migrations.ts

export async function migrateUsersV1ToV2(tx) {
  const users = tx.table("users");

  for await (const user of users) {
    if (!user.role) {
      user.role = "user";
      await users.put(user);
    }
  }
}
// db/modules/orders/migrations.ts

export async function migrateOrdersV1ToV2(tx) {
  const orders = tx.table("orders");

  for await (const order of orders) {
    if (!order.status) {
      order.status = "pending";
      await orders.put(order);
    }
  }
}

Каждый модуль определяет только собственные изменения данных, не затрагивая остальные таблицы.

Координация версий схемы

Проблема модульного подхода заключается в необходимости синхронизации версий. Dexie.js использует инкрементальные версии базы, поэтому композиция миграций должна учитывать порядок изменений.

Один из распространённых подходов — централизованный реестр версий:

const migrationsV2 = [
  migrateUsersV1ToV2,
  migrateOrdersV1ToV2
];

И выполнение внутри upgrade:

this.version(2).upgrade(async tx => {
  for (const migration of migrationsV2) {
    await migration(tx);
  }
});

При этом каждый модуль остаётся автономным, но подключается к общей цепочке обновлений.

Динамическое расширение схемы

В более сложных системах допускается динамическая регистрация модулей:

const modules = [
  usersModule,
  ordersModule,
  logsModule
];

const schema = modules.reduce((acc, module) => {
  return { ...acc, ...module.schema };
}, {});

И аналогично для миграций:

const migrations = modules.flatMap(m => m.migrations || []);

Такой подход позволяет подключать функциональные блоки без изменения ядра базы данных.

Разделение ответственности внутри модуля

Каждый модуль может содержать дополнительные слои:

  • schema.ts — описание таблиц;
  • indexes.ts — вспомогательные индексы и их документация;
  • migrations.ts — обновления структуры и данных;
  • seeds.ts — начальные данные;
  • repository.ts — доступ к данным.

В контексте Dexie.js это позволяет обернуть таблицы в доменные репозитории, не смешивая их с глобальной схемой.

Версионирование модулей

Каждый модуль может иметь собственную внутреннюю версию, независимую от версии базы:

export const USERS_MODULE_VERSION = 2;

Сопоставление с версией базы выполняется через таблицу соответствий:

const versionMap = {
  1: { users: 1, orders: 1 },
  2: { users: 2, orders: 1 }
};

Такой слой абстракции позволяет масштабировать схему без полной переработки истории миграций.

Типичные ошибки при модульной схеме

При неправильной организации возникают следующие проблемы:

  • дублирование таблиц в разных модулях;
  • неявные зависимости между модулями;
  • миграции, изменяющие чужие таблицы;
  • отсутствие централизованного контроля версий;
  • конфликт индексов при слиянии схем.

Dexie.js не накладывает ограничений на архитектуру схемы, поэтому дисциплина модульности полностью определяется структурой кода.

Композиция как архитектурный слой

Модульная схема становится промежуточным слоем между бизнес-доменом и физическим хранилищем. В такой архитектуре:

  • доменные модули управляют своими данными;
  • слой композиции агрегирует схемы;
  • Dexie выполняет финальную реализацию в IndexedDB.

Это позволяет масштабировать структуру данных без централизованных изменений, сохраняя предсказуемость миграций и изоляцию доменов.