Работа с несколькими базами данных в одном приложении

Модель изолированных хранилищ в браузере

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

Ключевой момент: один экземпляр Dexie = одна база данных IndexedDB. Следовательно, работа с несколькими базами сводится к управлению несколькими экземплярами Dexie в одном приложении.


Создание нескольких баз данных

Каждая база определяется уникальным именем при создании экземпляра:

import Dexie from "dexie";

const userDB = new Dexie("user_database");
userDB.version(1).stores({
  profiles: "id, name, email"
});

const logsDB = new Dexie("logs_database");
logsDB.version(1).stores({
  events: "++id, type, timestamp"
});

В этом примере создаются две независимые базы:

  • user_database — хранение пользовательских данных
  • logs_database — хранение событий и логов

Каждая база имеет собственные версии схем, транзакции и индексы.


Причины разделения данных на несколько баз

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

Изоляция контекстов

Разные доменные области приложения могут иметь несовместимые модели данных. Например:

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

Разделение снижает риск конфликтов миграций и упрощает поддержку схем.


Независимые циклы миграций

Каждая база имеет собственный versioning:

const analyticsDB = new Dexie("analytics");

analyticsDB.version(1).stores({
  hits: "++id, url, createdAt"
});

analyticsDB.version(2).stores({
  hits: "++id, url, createdAt, userId"
});

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


Масштабирование архитектуры

При росте приложения разделение баз упрощает:

  • вынос модулей в отдельные пакеты
  • ленивую инициализацию
  • разделение ответственности между командами

Управление несколькими экземплярами Dexie

Централизованная фабрика баз данных

Практика использования фабрики позволяет контролировать создание и повторное использование экземпляров:

import Dexie from "dexie";

const dbRegistry = new Map();

export function getDB(name) {
  if (dbRegistry.has(name)) {
    return dbRegistry.get(name);
  }

  const db = new Dexie(name);
  dbRegistry.set(name, db);

  return db;
}

Использование:

const userDB = getDB("user_database");
const logsDB = getDB("logs_database");

Такой подход предотвращает дублирование соединений и облегчает тестирование.


Singleton-подход для каждой базы

Каждая база может иметь собственный singleton:

import Dexie from "dexie";

let instance = null;

export function getUserDB() {
  if (instance) return instance;

  instance = new Dexie("user_database");

  instance.version(1).stores({
    profiles: "id, name, email"
  });

  return instance;
}

Транзакции между базами данных

IndexedDB не поддерживает транзакции, охватывающие несколько баз данных. Это означает:

  • нельзя атомарно обновить две базы одновременно
  • нельзя откатить изменения в разных базах единым действием

Компенсационные операции

Для согласованности применяется паттерн компенсации:

async function updateUserAndLog(user) {
  try {
    await userDB.profiles.put(user);

    await logsDB.events.add({
      type: "USER_UPDATED",
      timestamp: Date.now()
    });
  } catch (e) {
    await userDB.profiles.delete(user.id);
    throw e;
  }
}

Сценарии разделения баз

По функциональным модулям

  • auth_db
  • profile_db
  • cache_db

Такое разделение удобно при модульной архитектуре frontend-приложений.


По уровню чувствительности данных

Отдельная база может использоваться для:

  • публичных кэшированных данных
  • приватных пользовательских данных
  • временных данных с коротким TTL

По режимам работы

  • online-синхронизация
  • offline-first кэш
  • аналитика локальных событий

Версионирование в мульти-базовой архитектуре

Каждая база обновляется независимо:

const cacheDB = new Dexie("cache");

cacheDB.version(1).stores({
  responses: "url, data"
});

cacheDB.version(2).stores({
  responses: "url, data, ttl"
});

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


Синхронизация данных между базами

Явное копирование

const user = await userDB.profiles.get(1);

await cacheDB.responses.put({
  url: "/profile/1",
  data: user
});

Событийная синхронизация

Можно использовать внутреннюю шину событий:

function emitChange(event) {
  window.dispatchEvent(new CustomEvent("db-change", { detail: event }));
}

userDB.profiles.hook("creating", (primKey, obj) => {
  emitChange({ type: "create", table: "profiles", obj });
});

Производительность при множестве баз

Каждая база IndexedDB создает собственные:

  • connection pool
  • metadata cache
  • transaction queue

При большом количестве баз возможны накладные расходы:

  • увеличение времени открытия приложения
  • рост потребления памяти
  • конкуренция за I/O поток

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


Индексация и дублирование схем

Разные базы не разделяют индексы, поэтому при необходимости кросс-доменных запросов возникает дублирование:

  • одинаковые индексы в разных базах
  • повторное хранение данных для разных сценариев доступа

Это повышает скорость чтения, но увеличивает объем хранилища.


Тестирование мульти-базовой архитектуры

Изоляция тестовых баз

const testUserDB = new Dexie("test_user_db");
const testLogsDB = new Dexie("test_logs_db");

Перед каждым тестом базы очищаются:

await testUserDB.delete();
await testLogsDB.delete();

Эфемерные базы

Для тестирования часто создаются временные базы с уникальными именами:

const db = new Dexie(`temp_db_${Date.now()}`);

Ошибки и ограничения при работе с несколькими базами

Конкуренция за блокировку IndexedDB

Одновременные операции в нескольких базах могут приводить к:

  • задержкам транзакций
  • очередям запросов
  • деградации производительности

Несогласованность данных

Отсутствие общей транзакционности приводит к ситуациям:

  • данные обновлены в одной базе, но не в другой
  • частично выполненные операции

Проблемы миграций

При изменении логики приложения:

  • одна база обновлена
  • другая осталась в старой версии

Это требует координации миграций на уровне приложения.


Архитектурные паттерны использования нескольких баз

Feature-based databases

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

  • уведомления
  • чат
  • аналитика

Layered storage

  • primary DB — основное хранилище
  • cache DB — ускоренный доступ
  • sync DB — буфер синхронизации

Domain isolation pattern

Каждая предметная область полностью изолирована, включая:

  • таблицы
  • индексы
  • миграции
  • lifecycle управления

Управление жизненным циклом баз

Инициализация нескольких баз должна быть контролируемой:

  • ленивое создание при первом обращении
  • закрытие неиспользуемых соединений
  • централизованный cleanup при logout или reset приложения

Практическая структура приложения

const dbs = {
  user: new Dexie("user_db"),
  logs: new Dexie("logs_db"),
  cache: new Dexie("cache_db")
};

dbs.user.version(1).stores({
  profiles: "id, name"
});

dbs.logs.version(1).stores({
  events: "++id, type"
});

dbs.cache.version(1).stores({
  responses: "url"
});

Такой подход формирует явную границу ответственности и упрощает сопровождение системы.