При работе с Dexie.js объединение данных из нескольких object store чаще всего выполняется на уровне приложения, поскольку IndexedDB не поддерживает классические SQL-join операции. Это означает, что разработчик самостоятельно управляет загрузкой наборов данных, их сопоставлением, дедупликацией и финальной сборкой результата.
Типичная структура базы в Dexie.js включает несколько таблиц, например:
users — информация о пользователяхorders — заказыproducts — товарыlogs — события или действияКаждая таблица изолирована, и связи между ними выражаются через ключи:
userIdproductIdorderIdОтсутствие автоматических 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 позволяет заменить 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 позволяет строить цепочки запросов, которые затем объединяются вручную:
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 формируется вокруг трех базовых этапов:
Этот подход заменяет отсутствующие join-механизмы и позволяет строить сложные объектные графы поверх IndexedDB без потери контроля над производительностью и структурой данных.