Модель «один-ко-многим» в 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();
Данный подход сохраняет независимость таблиц и позволяет гибко управлять загрузкой данных.
Часто используется объединение данных на уровне приложения:
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 имеет архитектурные ограничения:
Эти ограничения компенсируются гибкостью схемы и контролем на уровне бизнес-логики.
При усложнении модели «один-ко-многим» может трансформироваться в гибридные структуры:
parentIdПример иерархической структуры:
db.version(1).stores({
categories: '++id, parentId, name'
});
Запрос дочерних элементов:
const children = await db.categories
.where('parentId')
.equals(1)
.toArray();
Такая модель часто используется совместно с «один-ко-многим» для построения сложных графов данных.