В основе Dexie.js лежит IndexedDB, где каждая таблица опирается на первичный ключ и, при наличии, уникальные индексы. Любая попытка вставки данных с уже существующим ключом приводит к нарушению ограничения уникальности.
В пакетных операциях это проявляется особенно резко: при массовой вставке даже одного конфликтующего элемента может измениться результат всей операции.
Основные источники конфликтов:
keyPath или автоинкремент)unique index)Dexie предоставляет два основных метода массовой записи:
bulkAdd() — строгая вставка новых записейbulkPut() — вставка с заменой существующихКлючевое различие заключается в поведении при столкновении с дубликатами.
bulkAdd():
bulkPut():
Дубликаты могут возникать на нескольких уровнях:
Внутри входного массива
table.bulkAdd([
{ id: 1, name: "A" },
{ id: 1, name: "B" }
]);
Конфликт происходит ещё до обращения к базе.
С уже существующими данными
table.bulkAdd([
{ id: 1 },
{ id: 2 }
]);
Если id=1 уже существует — возникает нарушение
уникальности.
По уникальным индексам Даже при уникальном primary key возможны конфликты:
db.users.defineIndex("email", "email", { unique: true });При конфликте Dexie формирует специализированную ошибку:
Dexie.BulkErrorОна содержит:
failures — массив элементов, которые не удалось
вставитьinner — первичную IndexedDB ошибку
(ConstraintError)Типичный сценарий:
Это делает пакетные операции частично атомарными только в рамках транзакции.
При использовании транзакции:
db.transaction("rw", db.table, async () => {
await db.table.bulkAdd(items);
});
при выбросе исключения:
Без транзакции возможен частичный коммит.
Самый дешёвый по ресурсам подход — очистка массива до вставки.
const unique = Array.from(
new Map(items.map(item => [item.id, item])).values()
);
Поведение:
Недостаток:
bulkPut вместо bulkAddawait table.bulkPut(items);
Поведение:
Особенности:
Используется при необходимости строгого контроля:
const existingIds = await table.where("id").anyOf(ids).primaryKeys();
const existingSet = new Se t(existingIds);
const filtered = items.filter(x => !existingSet.has(x.id));
await table.bulkAdd(filtered);
Плюсы:
Минусы:
BulkErrorПри частичной вставке важно разбирать результат:
try {
await table.bulkAdd(items);
} catch (e) {
if (e.name === "BulkError") {
console.log("Ошибки вставки:", e.failures);
}
}
Структура failures содержит:
Это позволяет реализовать повторную обработку:
bulkPutКомбинация add и игнорирования конфликтов:
for (const item of items) {
try {
await table.add(item);
} catch (e) {
if (e.name !== "ConstraintError") throw e;
}
}
Используется редко из-за низкой производительности, но даёт точный контроль.
Оптимизированный вариант:
const ids = items.map(x => x.id);
const existing = await table.where("id").anyOf(ids).primaryKeys();
const existingSet = new Se t(existing);
const toAdd = [];
const toUpdate = [];
for (const item of items) {
if (existingSet.has(item.id)) {
toUpdate.push(item);
} else {
toAdd.push(item);
}
}
await table.bulkAdd(toAdd);
await table.bulkPut(toUpdate);
Характер поведения:
Даже при уникальном primary key дубликаты могут возникать через вторичные индексы:
db.users = new Dexie.Table({
id: "++id",
email: { unique: true }
});
При пакетной вставке:
ConstraintErrorBulkErrorОсобенность:
При использовании ++id:
В пакетных методах возможны состояния:
Важно учитывать:
failures соответствует входному массивуДля высоконагруженных сценариев применяются следующие принципы:
bulkPut вместо сложной логики
проверкиПример батчинга:
const chunkSize = 500;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
await table.bulkPut(chunk);
}
При использовании bulkPut или комбинированной стратегии
система фактически переходит в модель:
Ключевые принципы:
BulkError требуют постобработкиРабота с дублирующими ключами в Dexie.js сводится к управлению компромиссом между производительностью пакетных операций и строгостью контроля целостности данных.