Каскадное удаление в Dexie.js строится вокруг идеи поддержания согласованности данных между связанными таблицами при удалении родительских записей. В отличие от реляционных СУБД, IndexedDB не предоставляет встроенных внешних ключей и автоматических каскадных операций, поэтому вся логика поддержания целостности реализуется на уровне приложения.
В приложениях, использующих Dexie.js, данные часто структурируются по принципу связанных сущностей. Типичный пример — пользователи и их заказы:
Без каскадного удаления возникает проблема «осиротевших записей»: при удалении пользователя его заказы остаются в базе, но теряют смысл, поскольку ссылка на пользователя больше не существует.
Такая ситуация приводит к:
Dexie.js не навязывает стратегию решения, но предоставляет инструменты для построения каскадного поведения вручную.
Типичная схема с отношением «один-ко-многим»:
const db = new Dexie("shop");
db.version(1).stores({
users: "++id, name",
orders: "++id, userId, total"
});
Здесь orders.userId выступает логической ссылкой на
users.id. Dexie не контролирует эту связь автоматически,
поэтому каскадное удаление реализуется вручную.
Самый прямолинейный способ — явное удаление связанных записей внутри транзакции.
db.transaction("rw", db.users, db.orders, async () => {
const userId = 1;
await db.orders.where("userId").equals(userId).delete();
await db.users.delete(userId);
});
Ключевые особенности подхода:
rw (readwrite) для обеих таблицЭтот способ прост, но требует дисциплины: разработчик обязан помнить о необходимости каскада в каждом месте удаления.
Dexie предоставляет механизм hooks, позволяющий перехватывать операции удаления и расширять их поведение.
db.users.hook("deleting", async (primKey, obj, trans) => {
await db.orders
.where("userId")
.equals(primKey)
.delete();
});
Здесь логика каскада переносится на уровень модели:
Важно учитывать, что hook выполняется внутри транзакции, поэтому все операции остаются атомарными.
deleting срабатывает до удаления записиВ более сложных архитектурах каскадное удаление выносится в отдельный слой:
class UserRepository {
async deleteUser(userId) {
return db.transaction("rw", db.users, db.orders, async () => {
await db.orders.where("userId").equals(userId).delete();
await db.users.delete(userId);
});
}
}
Преимущества:
Недостаток — необходимость строгого соблюдения архитектурных правил всеми частями приложения.
Реальные приложения часто имеют несколько уровней связей:
В этом случае каскад становится цепочкой удалений:
db.users.hook("deleting", async (userId, user, trans) => {
const orders = await db.orders.where("userId").equals(userId).toArray();
for (const order of orders) {
await db.orderItems.where("orderId").equals(order.id).delete();
await db.shipments.where("orderId").equals(order.id).delete();
}
await db.orders.where("userId").equals(userId).delete();
});
Здесь важно соблюдать порядок:
Нарушение порядка приводит к необходимости многократных проходов по данным или временным несоответствиям.
При больших объёмах данных каскадные операции могут становиться узким местом. Dexie позволяет оптимизировать их несколькими способами.
Вместо удаления по одному ключу предпочтительно использовать bulk-операции:
const orderIds = await db.orders
.where("userId")
.equals(userId)
.primaryKeys();
await db.orderItems.where("orderId").anyOf(orderIds).delete();
await db.orders.where("userId").equals(userId).delete();
Каждый вызов where() — это отдельная операция IndexedDB.
Поэтому важно:
anyOfЭффективность каскада напрямую зависит от наличия индексов:
db.version(1).stores({
users: "++id, name",
orders: "++id, userId, total",
orderItems: "++id, orderId"
});
Без индекса userId каждая операция каскада превращается
в полное сканирование таблицы orders, что критично ухудшает
производительность.
Индексы позволяют:
В сложных схемах возможно зацикливание логики удаления. Например:
Без контроля каскад может привести к бесконечным вызовам hooks.
Для предотвращения используют:
db.users.hook("deleting", async (id, obj, trans) => {
if (trans.cascadeRunning) return;
trans.cascadeRunning = true;
await db.orders.where("userId").equals(id).delete();
});
Технические таблицы часто исключаются из каскада или очищаются отдельно.
При удалении больших наборов данных (например, удаление всех пользователей региона) каскад должен быть оптимизирован под batch-обработку:
await db.transaction("rw", db.users, db.orders, async () => {
const userIds = await db.users
.where("region")
.equals("EU")
.primaryKeys();
await db.orders.where("userId").anyOf(userIds).delete();
await db.users.where("region").equals("EU").delete();
});
Такой подход уменьшает количество транзакций и снижает вероятность блокировок IndexedDB.
На практике часто встречаются следующие проблемы:
Удаление происходит в разных местах приложения без единых правил, что приводит к частично очищенным данным.
// антипаттерн
for (const order of orders) {
await db.orders.delete(order.id);
}
Без транзакции каскадные операции становятся неатомарными и могут оставлять базу в промежуточном состоянии.
Чем длиннее цепочка каскада, тем выше риск ошибок и сложнее сопровождение.
В реальных системах часто используется комбинация подходов:
Такая архитектура позволяет балансировать между автоматизацией и контролем.
Если внутри транзакции возникает ошибка:
Это делает каскадное удаление безопасным с точки зрения целостности, но требует аккуратной обработки исключений в hooks.
При проектировании каскадного удаления в Dexie.js ключевыми факторами становятся: