Вставка с перезаписью: put()

Метод put() в Dexie.js реализует операцию вставки с возможным обновлением (upsert) в таблицах IndexedDB. Его ключевая особенность заключается в том, что он не требует предварительной проверки существования записи: если объект с указанным первичным ключом уже присутствует в хранилище, запись будет перезаписана, иначе — добавлена как новая.

Внутренне put() опирается на механизм IndexedDB put операции объекта хранилища, что обеспечивает атомарность и предсказуемое поведение при работе с ключами.


Базовое поведение

При вызове put() происходит следующее:

  • выполняется определение первичного ключа объекта;
  • если ключ существует в таблице — соответствующая запись полностью заменяется;
  • если ключ отсутствует — создаётся новая запись;
  • операция выполняется асинхронно и возвращает Promise.

Простейшая форма использования:

await db.users.put({
  id: 1,
  name: "Alex",
  age: 30
});

Если в таблице users уже есть запись с id = 1, она будет полностью заменена новым объектом.


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

put() возвращает Promise, который резолвится в значение первичного ключа вставленной или обновлённой записи.

const key = await db.users.put({
  id: 5,
  name: "Maria"
});

Если первичный ключ задан явно (id: 5), возвращаемым значением будет 5. Если используется автоинкремент, будет возвращён сгенерированный ключ.


Отличие put() от add()

Разница между put() и add() принципиальна и влияет на поведение при конфликте ключей.

add()

  • добавляет только новые записи;
  • при наличии записи с таким же ключом выбрасывает ошибку;
  • используется для строгого контроля уникальности.

put()

  • выполняет вставку или замену;
  • не вызывает ошибки при существующем ключе;
  • обеспечивает идемпотентное обновление.
await db.users.add({ id: 1, name: "A" }); // ошибка, если id=1 уже существует
await db.users.put({ id: 1, name: "A" }); // безопасное обновление

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

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

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

В этом случае id является ключевым полем. При использовании put():

  • если id присутствует в объекте — он используется как ключ;
  • если отсутствует — поведение зависит от схемы (автоинкремент или нет).

Автоинкрементные ключи

При использовании ++id Dexie позволяет автоматически генерировать ключи:

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

Тогда:

await db.users.put({
  name: "John",
  age: 25
});

поведёт себя следующим образом:

  • если объект не содержит id, он будет сгенерирован автоматически;
  • при повторном put() с тем же id запись будет перезаписана.

Полная замена записи

Важно понимать, что put() заменяет запись целиком, а не частично.

await db.users.put({
  id: 10,
  name: "Anna"
});

Если ранее объект выглядел так:

{
  id: 10,
  name: "Anna",
  age: 40,
  city: "Almaty"
}

после put():

{
  id: 10,
  name: "Anna"
}

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


Частичное обновление и ограничения

put() не предназначен для частичного обновления. Для таких сценариев используется:

  • update()
  • modify() (Dexie Collection API)

Попытка использовать put() как патч-операцию приводит к потере данных.


Bulk-операции: bulkPut()

Dexie.js предоставляет оптимизированный вариант массовой вставки/обновления:

await db.users.bulkPut([
  { id: 1, name: "A" },
  { id: 2, name: "B" },
  { id: 3, name: "C" }
]);

Особенности:

  • работает быстрее, чем последовательные put();
  • выполняется в рамках одной транзакции;
  • при ошибке поведение зависит от транзакционной модели (часто откат всей операции).

Использование в транзакциях

put() часто применяется внутри транзакций Dexie:

await db.transaction("rw", db.users, async () => {
  await db.users.put({ id: 1, name: "Updated" });
  await db.users.put({ id: 2, name: "Another" });
});

Характеристики:

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

Поведение при конфликте данных

Конфликт в put() трактуется не как ошибка, а как замена записи. Однако ошибки могут возникать в случаях:

  • нарушение уникальных индексов;
  • некорректный формат ключа;
  • превышение лимитов IndexedDB;
  • проблемы транзакции.

Пример конфликта уникального индекса:

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

Если email объявлен как уникальный индекс, попытка вставить дубликат вызовет исключение даже при использовании put().


Асинхронная модель выполнения

put() возвращает Promise и всегда выполняется асинхронно:

db.users.put({ id: 1, name: "Test" })
  .then(key => {
    console.log("Saved key:", key);
  })
  .catch(err => {
    console.error("Error:", err);
  });

Dexie оборачивает IndexedDB API в промисы, обеспечивая удобную цепочку обработки.


Использование с возвращаемыми данными

Хотя put() возвращает только ключ, часто требуется получить обновлённый объект:

await db.users.put({ id: 1, name: "Updated" });
const user = await db.users.get(1);

Это важно, поскольку put() не возвращает сохранённый объект целиком.


Работа с индексами

put() учитывает все индексы таблицы. При сохранении:

  • обновляются вторичные индексы;
  • пересчитываются уникальные ограничения;
  • запись становится доступной для запросов через where().

Типичные сценарии использования

Синхронизация данных с сервером

await db.users.bulkPut(serverUsers);

Используется для зеркалирования состояния backend.

Кэширование сущностей

await db.cache.put({
  key: "profile_1",
  data: profileData,
  updatedAt: Date.now()
});

Обновление сущности без проверки существования

await db.settings.put({
  id: "theme",
  value: "dark"
});

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

Потеря данных из-за полного перезаписывания

await db.users.put({ id: 1, name: "OnlyName" });

Если объект содержал другие поля, они будут удалены.


Отсутствие ключа в схеме

Если таблица не определяет корректный первичный ключ:

db.version(1).stores({
  users: "name" // нет уникального id
});

put() может вести себя неожиданно при дубликатах значений name.


Использование put() вместо частичного обновления

await db.users.put({
  id: 1,
  age: 31
});

Приведёт к потере всех остальных полей записи.


Производительность

put() оптимизирован для массовых операций, но его производительность зависит от:

  • количества индексов;
  • размера объектов;
  • частоты вызовов;
  • использования bulkPut() вместо циклов.

bulkPut() почти всегда предпочтительнее при пакетной записи.


Особенности реализации в Dexie.js

Dexie добавляет поверх IndexedDB:

  • автоматическое управление транзакциями;
  • Promise-обёртку;
  • нормализацию ошибок;
  • удобный API поверх IDBObjectStore.put.

Это делает put() более предсказуемым по сравнению с нативным IndexedDB API, где требуется ручное управление транзакциями и событиями onsuccess/onerror.


Поведение при авто-генерации ключей и повторных вызовах

При повторных вызовах put() с теми же данными:

  • запись полностью перезаписывается;
  • ключ сохраняется (если указан явно);
  • автоинкремент не пересоздаётся.

Взаимодействие с наблюдателями (Hooks)

Dexie поддерживает hooks (creating, updating, deleting), которые могут быть вызваны при put():

  • creating — при вставке новой записи;
  • updating — при обновлении существующей.

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