Добавление записей: add()

Операция добавления данных в Dexie.js реализуется через метод add(), который предоставляет высокоуровневый интерфейс для вставки новой записи в таблицу IndexedDB. В отличие от низкоуровневого API IndexedDB, где требуется создание запросов, обработка событий успеха и ошибок, Dexie.js инкапсулирует весь процесс в один вызов, возвращающий Promise.

Метод add() работает строго в контексте таблицы, то есть вызывается на экземпляре Table:

db.users.add({ name: "Alex", age: 25 });

Результатом выполнения является Promise, который резолвится в первичный ключ добавленной записи.


Сигнатура и базовое поведение

Основная сигнатура метода:

table.add(item, [key])
  • item — объект, который будет сохранён в таблице
  • key — необязательный параметр, задающий значение первичного ключа вручную (если схема позволяет)

Возвращаемое значение:

  • Promise<PrimaryKey> — ключ добавленной записи

Пример:

const id = await db.users.add({
  name: "Maria",
  age: 31
});

console.log(id);

Если таблица использует автоинкрементный ключ, Dexie автоматически сгенерирует его.


Работа с первичным ключом

Dexie.js опирается на схему таблицы, заданную через stores(). Например:

const db = new Dexie("AppDB");

db.version(1).stores({
  users: "++id, name, age"
});

Здесь:

  • ++id означает автоинкрементный первичный ключ
  • name, age — индексируемые поля

При добавлении записи:

await db.users.add({
  name: "Ivan",
  age: 40
});

поле id будет сгенерировано автоматически.

Если же ключ передан явно:

await db.users.add(
  { name: "Ivan", age: 40 },
  1001
);

Dexie попытается использовать 1001 как primary key. При конфликте возникнет ошибка.


Поведение при дублировании ключа

Одна из ключевых особенностей add() — строгое предотвращение перезаписи существующих записей.

Если ключ уже существует, операция завершится ошибкой:

Dexie.ConstraintError: Key already exists in the object store

Пример:

await db.users.add({ id: 1, name: "A" });
await db.users.add({ id: 1, name: "B" }); // ошибка

Это принципиальное отличие от метода put(), который выполняет вставку или обновление.


Различие между add() и put()

Хотя оба метода используются для записи данных, их семантика различается:

  • add() — только вставка, без перезаписи
  • put() — вставка или обновление
await db.users.add({ id: 1, name: "Alex" }); // ошибка при повторе
await db.users.put({ id: 1, name: "Alex" }); // безопасно обновит

add() применяется в случаях, где требуется гарантия уникальности записи и строгая целостность данных.


Асинхронное поведение и Promise

Метод возвращает Promise, что позволяет использовать async/await:

async function createUser() {
  const id = await db.users.add({
    name: "Olga",
    age: 28
  });

  return id;
}

При использовании цепочек:

db.users.add({ name: "Pavel" })
  .then(id => console.log("Created:", id))
  .catch(err => console.error(err));

Ошибки обрабатываются стандартным механизмом Promise rejection.


Добавление в транзакциях

Dexie позволяет выполнять add() внутри транзакций, что обеспечивает атомарность операций.

await db.transaction("rw", db.users, async () => {
  await db.users.add({ name: "User1" });
  await db.users.add({ name: "User2" });
});

Если одна из операций завершится ошибкой, вся транзакция будет откатана.

Это особенно важно при массовой вставке связанных данных.


Работа с индексами при добавлении

При вызове add() Dexie не только сохраняет объект, но и обновляет все индексированные поля, определённые в схеме:

db.version(1).stores({
  users: "++id, email, age"
});

Добавление:

await db.users.add({
  email: "test@example.com",
  age: 22
});

После вставки:

  • запись попадает в primary storage
  • индекс email обновляется
  • индекс age обновляется

Важно, что IndexedDB требует, чтобы индексированные поля существовали в объекте, иначе они будут сохранены как undefined, что может повлиять на поиск.


Ограничения add()

Метод имеет ряд ограничений, обусловленных как IndexedDB, так и реализацией Dexie:

  1. Нельзя обновлять существующую запись
  2. Нельзя частично вставлять данные — сохраняется весь объект
  3. Нельзя вставлять данные без соблюдения схемы индексов
  4. При ошибке вся операция отклоняется

Пример некорректного использования:

await db.users.add({ name: "NoAgeUser" });
// если age требуется логикой приложения — это приведёт к неполным данным

Поведение при автоинкременте

При использовании ++primaryKey Dexie делегирует генерацию ключа IndexedDB:

db.version(1).stores({
  logs: "++id, message"
});

Добавление:

await db.logs.add({ message: "System started" });

Происходит:

  • генерация нового id
  • вставка объекта
  • возврат сгенерированного ключа

Этот механизм гарантирует уникальность даже при параллельных вставках.


Возврат значения и его использование

Возвращаемое значение add() — это первичный ключ, который можно использовать сразу после вставки:

const userId = await db.users.add({
  name: "Sergey"
});

const user = await db.users.get(userId);

Это позволяет строить цепочки зависимых операций без дополнительного поиска.


Массовое добавление и альтернатива bulkAdd

Хотя add() предназначен для одиночной вставки, при большом количестве данных его использование становится неэффективным.

await db.users.add({ name: "A" });
await db.users.add({ name: "B" });
await db.users.add({ name: "C" });

Dexie предоставляет специализированный метод bulkAdd(), оптимизированный для пакетной вставки, который снижает количество транзакционных операций.

Тем не менее add() остаётся базовым строительным блоком для единичных операций.


Ошибки и их обработка

Основные ошибки при использовании:

  • ConstraintError — нарушение уникальности ключа
  • DataError — некорректный формат данных
  • TransactionInactiveError — попытка записи вне активной транзакции
  • InvalidStateError — некорректное состояние базы

Пример обработки:

try {
  await db.users.add({ id: 1, name: "Test" });
} catch (e) {
  if (e.name === "ConstraintError") {
    console.log("Запись с таким ключом уже существует");
  }
}

Особенности сериализации объекта

Dexie использует structured clone algorithm, что означает:

  • поддерживаются вложенные объекты
  • поддерживаются массивы
  • не поддерживаются функции
  • не поддерживаются классовые экземпляры с методами

Пример:

await db.users.add({
  name: "Anna",
  profile: {
    city: "Almaty",
    active: true
  }
});

Такая структура будет корректно сохранена в IndexedDB.


Поведение при отсутствии схемы поля

Dexie не требует строгой схемы объектов, но индексы должны быть заранее определены.

Если поле не индексировано, оно всё равно сохраняется, но не участвует в поиске:

db.version(1).stores({
  users: "++id, name"
});
await db.users.add({
  name: "Dmitry",
  metadata: { role: "admin" }
});

Поле metadata будет сохранено, но не индексировано.


Асинхронная конкурентность

Dexie управляет конкурентным доступом к IndexedDB автоматически. При одновременных вызовах:

db.users.add({ name: "A" });
db.users.add({ name: "B" });
db.users.add({ name: "C" });

операции будут сериализованы внутри транзакции браузера, сохраняя целостность данных.