ConstraintError — одна из наиболее распространённых ошибок при работе с IndexedDB и Dexie.js. Она возникает в ситуациях, когда операция нарушает ограничения целостности данных, определённые схемой таблицы. На практике наиболее частой причиной становится попытка сохранить запись с уже существующим уникальным ключом или индексом.
Понимание механизма возникновения ConstraintError особенно важно при разработке приложений с синхронизацией данных, многопользовательскими сценариями, импортом больших объёмов информации и пакетными операциями записи.
В основе Dexie.js лежит IndexedDB, которая поддерживает различные ограничения:
Когда операция записи нарушает одно из этих ограничений, IndexedDB
генерирует исключение ConstraintError, которое Dexie
преобразует в собственный объект ошибки.
Простейший пример:
db.version(1).stores({
users: "id, email"
});
await db.users.add({
id: 1,
email: "user@example.com"
});
await db.users.add({
id: 1,
email: "another@example.com"
});
Вторая операция завершится ошибкой:
Dexie.ConstraintError
Причина очевидна: запись с ключом id = 1 уже
существует.
Первичный ключ является главным идентификатором записи внутри таблицы.
Схема:
db.version(1).stores({
products: "id, name"
});
Добавление объекта:
await db.products.add({
id: 100,
name: "Laptop"
});
Повторная вставка:
await db.products.add({
id: 100,
name: "Monitor"
});
Завершится ошибкой:
ConstraintError
Поскольку метод add() предполагает создание новой записи
и запрещает перезапись существующей.
Метод add() работает по принципу:
Добавить запись только если ключ отсутствует.
Если запись существует, операция считается нарушением ограничения.
Поведение напоминает SQL-конструкцию:
INS ERT IN TO table (...)
без поддержки обновления существующих строк.
Одна из наиболее распространённых причин появления ConstraintError — неправильный выбор метода записи.
await db.users.add({
id: 1,
name: "Alex"
});
Создаёт запись только при отсутствии ключа.
При наличии ключа:
ConstraintError
await db.users.put({
id: 1,
name: "Alex"
});
Поведение:
Поэтому:
await db.users.put({
id: 1,
name: "John"
});
не вызывает ошибок.
Схема с автоинкрементом:
db.version(1).stores({
posts: "++id, title"
});
Обычная вставка:
await db.posts.add({
title: "Article"
});
Идентификатор будет создан автоматически.
Однако ошибка возможна при явном указании уже существующего значения:
await db.posts.add({
id: 1,
title: "Duplicate"
});
Если запись с таким идентификатором существует, будет сгенерирован ConstraintError.
ConstraintError возникает не только для первичных ключей.
Dexie поддерживает уникальные вторичные индексы.
Пример:
db.version(1).stores({
users: "++id, &email"
});
Символ & означает уникальность индекса.
Первая запись:
await db.users.add({
email: "user@example.com"
});
Вторая запись:
await db.users.add({
email: "user@example.com"
});
Приведёт к ошибке.
Хотя идентификаторы различаются, значение уникального индекса совпадает.
Уникальные индексы позволяют гарантировать отсутствие дубликатов:
"&username"
"&email"
"&phone"
"&passportNumber"
Например:
db.version(1).stores({
accounts: `
++id,
&email,
&username
`
});
Теперь невозможно создать два аккаунта с одинаковым email или логином.
Наиболее распространённый способ обработки:
try {
await db.users.add(user);
}
catch (error) {
if (error instanceof Dexie.ConstraintError) {
console.log("Дубликат ключа");
}
}
Такой подход позволяет отделить ошибки уникальности от других проблем.
Иногда проверка выполняется через свойство name.
try {
await db.users.add(user);
}
catch (error) {
if (error.name === "ConstraintError") {
console.log("Обнаружен дубликат");
}
}
Этот вариант полезен при интеграции с кодом, который работает напрямую через IndexedDB.
Иногда ошибка предотвращается предварительной проверкой.
Пример:
const existing = await db.users.get(1);
if (!existing) {
await db.users.add({
id: 1,
name: "Alex"
});
}
Подход уменьшает вероятность появления ConstraintError.
Однако следует учитывать проблему конкурентного доступа.
Следующий код выглядит безопасным:
const exists = await db.users.get(1);
if (!exists) {
await db.users.add(user);
}
Но между проверкой и вставкой другая операция может создать запись с тем же ключом.
Сценарий:
get().add().add().Результат:
ConstraintError
Поэтому предварительная проверка не заменяет полноценную обработку ошибок.
Транзакция уменьшает вероятность логических конфликтов.
await db.transaction(
"rw",
db.users,
async () => {
const exists = await db.users.get(1);
if (!exists) {
await db.users.add(user);
}
}
);
Тем не менее даже внутри транзакции следует быть готовым к ConstraintError.
Рассмотрим пример:
await db.users.bulkAdd([
{ id: 1, name: "A" },
{ id: 2, name: "B" },
{ id: 1, name: "C" }
]);
Одна из записей содержит повторяющийся ключ.
Результатом станет ошибка.
Для пакетных операций Dexie использует специальное исключение.
try {
await db.users.bulkAdd(data);
}
catch (error) {
if (error instanceof Dexie.BulkError) {
console.log(error.failures);
}
}
Внутри объекта ошибки можно получить список проблемных записей.
Если требуется обновление существующих записей:
await db.users.bulkPut([
{ id: 1, name: "Alex" },
{ id: 2, name: "John" }
]);
Метод работает аналогично обычному put():
Поэтому дубликаты первичного ключа не вызывают ConstraintError.
Dexie поддерживает составные индексы.
Пример:
db.version(1).stores({
employees: `
++id,
&[department+employeeNumber]
`
});
Комбинация:
{
department: "IT",
employeeNumber: 100
}
должна быть уникальной.
Попытка создать вторую запись с тем же сочетанием значений приведёт к ConstraintError.
Мультииндексы используются для массивов.
Пример:
db.version(1).stores({
articles: `
++id,
*tags
`
});
Если такой индекс объявлен как уникальный:
"&*tags"
то каждая метка становится уникальным значением.
Попытка добавить запись с уже существующим тегом может вызвать ConstraintError.
Из-за сложности поведения подобные конструкции используются редко.
При импорте большого массива объектов дубликаты встречаются часто.
Опасный вариант:
await db.users.bulkAdd(importedUsers);
Без обработки ошибок операция может завершиться неудачей.
Более надёжный подход:
try {
await db.users.bulkAdd(importedUsers);
}
catch (error) {
if (error instanceof Dexie.BulkError) {
console.log(
`${error.failures.length} конфликтов`
);
}
}
for (const user of users) {
try {
await db.users.add(user);
}
catch (error) {
if (!(error instanceof Dexie.ConstraintError)) {
throw error;
}
}
}
Дубликаты просто пропускаются.
await db.users.put(user);
Существующая запись обновляется автоматически.
const existing = await db.users.get(user.id);
if (existing) {
await db.users.put({
...existing,
...user
});
}
else {
await db.users.add(user);
}
Подход часто используется при синхронизации.
Иногда конфликт решается созданием нового идентификатора.
await db.notes.add({
id: crypto.randomUUID(),
text: "Note"
});
Вероятность дублирования становится практически нулевой.
Полезно выводить полную информацию об ошибке.
try {
await db.users.add(user);
}
catch (error) {
console.error(error.name);
console.error(error.message);
}
Типичный вывод:
ConstraintError
A mutation operation in the transaction failed...
Текст сообщения зависит от браузера и реализации IndexedDB.
Повторная вставка существующего первичного ключа
add()
вместо
put()
Нарушение уникального индекса
&email
при сохранении одинаковых адресов электронной почты.
Ошибки импорта
Несколько объектов содержат одинаковые идентификаторы.
Конфликты синхронизации
Локальные и удалённые данные используют одинаковые ключи.
Конкурирующие операции записи
Несколько вкладок или процессов пытаются создать запись одновременно.
add() только тогда, когда существование
записи считается ошибкой.put() для сценариев обновления и
синхронизации.ConstraintError.BulkError.get().Понимание природы ConstraintError позволяет строить надёжные механизмы хранения данных, корректно обрабатывать конфликты записи и предотвращать появление повреждённых или дублирующихся записей в локальной базе данных IndexedDB.