Моделирование отношений один-ко-многим

Модель «один-ко-многим» в Dexie.js строится поверх ограничений IndexedDB, где отсутствуют настоящие внешние ключи и автоматическая поддержка реляционных связей. Связность данных достигается за счёт явного хранения идентификаторов родительских сущностей в дочерних таблицах и организации индексов, позволяющих эффективно выполнять выборки.

Классический вариант отношения «один-ко-многим» включает:

  • родительскую сущность (например, пользователь)
  • множество дочерних сущностей (например, заказы пользователя)

В Dexie.js это выражается двумя таблицами:

const db = new Dexie('appDB');

db.version(1).stores({
  users: '++id, name, email',
  orders: '++id, userId, createdAt, total'
});

Ключевым элементом модели выступает поле userId в таблице orders. Оно не является внешним ключом в строгом смысле, но логически выполняет его роль.

Индексация как основа связей

Эффективность модели напрямую зависит от наличия индекса по полю связи:

db.version(1).stores({
  users: '++id, name',
  orders: '++id, userId, status, createdAt'
});

Индекс userId позволяет выполнять выборку всех дочерних записей без полного сканирования таблицы:

const userOrders = await db.orders
  .where('userId')
  .equals(1)
  .toArray();

Отсутствие индекса приводит к полной итерации коллекции и ухудшению производительности при росте объёма данных.

Паттерн связывания данных через идентификаторы

Связь реализуется через сохранение идентификатора родителя при создании дочерней записи:

await db.orders.add({
  userId: 1,
  createdAt: new Date(),
  total: 250
});

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

Получение связных данных

Ленивое связывание

Самый простой способ — последовательные запросы:

const user = await db.users.get(1);

const orders = await db.orders
  .where('userId')
  .equals(user.id)
  .toArray();

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

Проекция через map

Часто используется объединение данных на уровне приложения:

const users = await db.users.toArray();

const usersWithOrders = await Promise.all(
  users.map(async user => {
    const orders = await db.orders
      .where('userId')
      .equals(user.id)
      .toArray();

    return {
      ...user,
      orders
    };
  })
);

Такой подход формирует структуру, приближенную к документной модели данных.

Оптимизация выборок

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

const orders = await db.orders
  .where('userId')
  .anyOf([1, 2, 3])
  .toArray();

Дальнейшая группировка выполняется в памяти:

const grouped = orders.reduce((acc, order) => {
  if (!acc[order.userId]) acc[order.userId] = [];
  acc[order.userId].push(order);
  return acc;
}, {});

Использование транзакций для целостности

Отсутствие встроенных внешних ключей компенсируется транзакциями:

await db.transaction('rw', db.users, db.orders, async () => {
  const userId = await db.users.add({
    name: 'Alex',
    email: 'alex@mail.com'
  });

  await db.orders.add({
    userId,
    total: 100,
    createdAt: new Date()
  });
});

Транзакция гарантирует атомарность операций и снижает риск частично записанных связей.

Удаление и каскадная логика

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

await db.transaction('rw', db.users, db.orders, async () => {
  await db.orders.where('userId').equals(1).delete();
  await db.users.delete(1);
});

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

Композитные индексы в связях

При расширенных сценариях модель «один-ко-многим» комбинируется с дополнительными критериями:

db.version(2).stores({
  orders: '++id, [userId+status], createdAt'
});

Это позволяет выполнять выборки с фильтрацией по нескольким полям:

const activeOrders = await db.orders
  .where('[userId+status]')
  .equals([1, 'active'])
  .toArray();

Паттерн «обратной навигации»

Хотя связь задаётся от дочерней сущности к родительской, часто требуется обратное представление:

async function getUserWithOrders(userId) {
  const [user, orders] = await Promise.all([
    db.users.get(userId),
    db.orders.where('userId').equals(userId).toArray()
  ]);

  return {
    ...user,
    orders
  };
}

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

Денормализация как способ ускорения

В некоторых случаях часть данных родителя дублируется в дочерней таблице:

await db.orders.add({
  userId: 1,
  userName: 'Alex',
  total: 200
});

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

Обновление связанных данных

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

await db.transaction('rw', db.users, db.orders, async () => {
  await db.users.update(1, { name: 'Alexander' });

  await db.orders
    .where('userId')
    .modify({ userName: 'Alexander' });
});

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

Массовые операции в отношениях

При работе с большими наборами данных используется пакетная обработка:

const users = await db.users.toArray();

for (const user of users) {
  const orders = await db.orders.where('userId').equals(user.id);

  await orders.modify({ processed: true });
}

Dexie.js оптимизирует такие операции через внутренние батчи IndexedDB.

Ограничения модели

Модель «один-ко-многим» в Dexie.js имеет архитектурные ограничения:

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

Эти ограничения компенсируются гибкостью схемы и контролем на уровне бизнес-логики.

Расширенные структуры связей

При усложнении модели «один-ко-многим» может трансформироваться в гибридные структуры:

  • один-ко-многим + многие-ко-многим через промежуточные таблицы
  • иерархические деревья через self-referencing parentId
  • денормализованные агрегаты для быстрого чтения

Пример иерархической структуры:

db.version(1).stores({
  categories: '++id, parentId, name'
});

Запрос дочерних элементов:

const children = await db.categories
  .where('parentId')
  .equals(1)
  .toArray();

Такая модель часто используется совместно с «один-ко-многим» для построения сложных графов данных.