Объединение результатов из нескольких хранилищ вручную

При работе с Dexie.js объединение данных из нескольких object store чаще всего выполняется на уровне приложения, поскольку IndexedDB не поддерживает классические SQL-join операции. Это означает, что разработчик самостоятельно управляет загрузкой наборов данных, их сопоставлением, дедупликацией и финальной сборкой результата.

Типичная структура базы в Dexie.js включает несколько таблиц, например:

  • users — информация о пользователях
  • orders — заказы
  • products — товары
  • logs — события или действия

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

  • userId
  • productId
  • orderId

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


Базовый подход: параллельная загрузка данных

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

const [users, orders, products] = await Promise.all([
  db.users.toArray(),
  db.orders.toArray(),
  db.products.toArray()
]);

После получения наборов данных формируется единая структура:

const result = {
  users,
  orders,
  products
};

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


Ручное сопоставление через Map-структуры

При объединении данных важно минимизировать сложность поиска. Использование Map позволяет заменить O(n²) на O(n).

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

const userMap = new Map();
users.forEach(user => {
  userMap.set(user.id, user);
});

Теперь можно быстро сопоставить заказы с пользователями:

const enrichedOrders = orders.map(order => {
  return {
    ...order,
    user: userMap.get(order.userId) || null
  };
});

Такой подход является базовой формой “ручного join”.


Объединение через несколько запросов по индексам

Dexie.js позволяет эффективно использовать индексы через where().

const orders = await db.orders.where('status').equals('pending').toArray();
const userIds = [...new Set(orders.map(o => o.userId))];

const users = await db.users.where('id').anyOf(userIds).toArray();

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


Построение связей “один ко многим”

Частый случай — пользователь и его заказы.

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

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

const ordersByUser = new Map();

orders.forEach(order => {
  if (!ordersByUser.has(order.userId)) {
    ordersByUser.set(order.userId, []);
  }
  ordersByUser.get(order.userId).push(order);
});

const result = users.map(user => {
  return {
    ...user,
    orders: ordersByUser.get(user.id) || []
  };
});

Этот подход превращает плоские таблицы в иерархическую структуру.


Объединение нескольких уровней связей

При наличии цепочек связей (user → orders → products) требуется каскадное сопоставление.

const [users, orders, products] = await Promise.all([
  db.users.toArray(),
  db.orders.toArray(),
  db.products.toArray()
]);

const productMap = new Map(products.map(p => [p.id, p]));

const ordersByUser = new Map();

for (const order of orders) {
  const enrichedOrder = {
    ...order,
    products: order.productIds.map(id => productMap.get(id)).filter(Boolean)
  };

  if (!ordersByUser.has(order.userId)) {
    ordersByUser.set(order.userId, []);
  }

  ordersByUser.get(order.userId).push(enrichedOrder);
}

const fullResult = users.map(user => ({
  ...user,
  orders: ordersByUser.get(user.id) || []
}));

Здесь происходит ручная реализация многослойного join-процесса.


Частичная загрузка и ленивое объединение

При больших данных полная загрузка невозможна. Используется ленивое объединение:

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

const userIds = users.map(u => u.id);

const orders = await db.orders
  .where('userId')
  .anyOf(userIds)
  .toArray();

Далее объединение выполняется только для ограниченного набора пользователей.


Объединение с агрегацией

Иногда требуется не просто объединение, а вычисление агрегатов.

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

const statsByUser = new Map();

for (const order of orders) {
  const current = statsByUser.get(order.userId) || {
    total: 0,
    count: 0
  };

  current.total += order.amount;
  current.count += 1;

  statsByUser.set(order.userId, current);
}

Затем результат связывается с пользователями:

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

const enriched = users.map(user => ({
  ...user,
  stats: statsByUser.get(user.id) || { total: 0, count: 0 }
}));

Дедупликация при объединении наборов

При объединении данных из нескольких источников часто возникают дубликаты. Используется Map или Set:

const combined = [...ordersFromA, ...ordersFromB];

const unique = new Map();

for (const order of combined) {
  unique.set(order.id, order);
}

const result = [...unique.values()];

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


Объединение через последовательные цепочки Dexie Collection

Dexie позволяет строить цепочки запросов, которые затем объединяются вручную:

const recentOrders = db.orders.where('date').above(Date.now() - 86400000);

const highValueOrders = db.orders.where('amount').above(1000);

const [a, b] = await Promise.all([
  recentOrders.toArray(),
  highValueOrders.toArray()
]);

const merged = [...a, ...b];

После объединения часто требуется дополнительная сортировка:

merged.sort((x, y) => y.amount - x.amount);

Стабильная сортировка после объединения

При объединении нескольких источников порядок теряется. Восстановление порядка выполняется явно:

const merged = [...setA, ...setB, ...setC];

merged.sort((a, b) => {
  return new Date(b.createdAt) - new Date(a.createdAt);
});

Для сложных случаев используется многоуровневая сортировка:

merged.sort((a, b) => {
  if (a.priority !== b.priority) return b.priority - a.priority;
  return b.createdAt - a.createdAt;
});

Оптимизация объединения через предварительные индексы

Ключевая стратегия — минимизация объема загружаемых данных через индексы Dexie:

const userIds = await db.orders
  .where('status')
  .equals('active')
  .primaryKeys();

Затем точечная загрузка:

const users = await db.users
  .where('id')
  .anyOf(userIds)
  .toArray();

Это снижает нагрузку на память и ускоряет объединение.


Итоговая структура объединения как паттерн

Ручное объединение данных в Dexie.js формируется вокруг трех базовых этапов:

  • получение ограниченных выборок через индексы
  • построение вспомогательных структур (Map, Set)
  • финальное сопоставление и агрегация

Этот подход заменяет отсутствующие join-механизмы и позволяет строить сложные объектные графы поверх IndexedDB без потери контроля над производительностью и структурой данных.