Первичные ключи: автоинкремент, явные, составные

В основе любой таблицы IndexedDB лежит первичный ключ (primary key), определяющий уникальность записей и обеспечивающий быстрый доступ к данным. В Dexie.js первичный ключ задаётся на уровне схемы таблицы и становится фундаментом всей модели хранения: он влияет на вставку, обновление, удаление и индексацию.

Первичный ключ в Dexie.js выполняет несколько критических функций:

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

В отличие от SQL-систем, IndexedDB и Dexie.js работают с объектными хранилищами, где первичный ключ может быть как простым, так и составным, а также автоматически генерируемым.


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

Автоинкрементный первичный ключ используется в случаях, когда уникальный идентификатор должен генерироваться автоматически при добавлении записи. В Dexie.js для этого применяется префикс ++ в описании схемы.

Пример определения:

const db = new Dexie('AppDB');

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

Здесь id становится первичным ключом с автоинкрементом.

Механика работы автоинкремента

IndexedDB поддерживает внутренний счётчик ключей, который:

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

Каждая новая запись без указанного ключа получает следующий доступный идентификатор:

await db.users.add({ name: 'Alice', email: 'a@mail.com' });
// id = 1

await db.users.add({ name: 'Bob', email: 'b@mail.com' });
// id = 2

Особенности автоинкрементных ключей

  • отсутствие гарантии плотной последовательности;
  • невозможность повторного использования удалённых значений;
  • независимость от бизнес-логики;
  • оптимальность для сущностей без естественного идентификатора.

Автоинкрементные ключи часто применяются в сценариях, где данные представляют собой сущности без внешнего идентификатора: локальные заметки, черновики, кэшированные объекты.


Явно задаваемые первичные ключи

Явный первичный ключ предполагает, что значение идентификатора задаётся приложением. В этом случае Dexie.js не генерирует ключ автоматически, а использует значение, переданное в объекте.

Пример:

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

Добавление записи:

await db.users.add({
  id: 'user_123',
  name: 'Alice',
  email: 'a@mail.com'
});

Типы явных ключей

Явные ключи могут быть:

Строковые идентификаторы

Часто применяются UUID, slug или внешние идентификаторы API:

id: '550e8400-e29b-41d4-a716-446655440000'

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

Используются, когда идентификатор приходит из внешней системы:

id: 100245

Особенности явных ключей

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

Явные ключи становятся предпочтительными при построении офлайн-first архитектур, где локальная база должна соответствовать серверной модели данных.


Составные первичные ключи

Составной первичный ключ представляет собой комбинацию нескольких полей, объединённых в единый уникальный идентификатор. В Dexie.js он задаётся через квадратные скобки:

db.version(1).stores({
  orders: '[userId+orderId], product, date'
});

Здесь первичный ключ состоит из двух частей:

  • userId
  • orderId

Логика работы составного ключа

IndexedDB хранит составной ключ как упорядоченную последовательность значений. Это означает:

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

Пример вставки:

await db.orders.add({
  userId: 1,
  orderId: 42,
  product: 'Keyboard'
});

Упорядочивание составных ключей

Составные ключи позволяют эффективно группировать данные:

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

Пример запроса диапазона:

db.orders
  .where('[userId+orderId]')
  .between([1, 0], [1, 9999])
  .toArray();

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


Отличие составного ключа от составного индекса

В Dexie.js важно различать:

Составной первичный ключ

'[a+b]'
  • определяет уникальность записи;
  • обязателен;
  • используется как основной идентификатор.

Составной индекс

'a,b'
  • не влияет на уникальность;
  • используется только для поиска;
  • может дублировать значения.

Пример:

db.version(1).stores({
  logs: '[userId+timestamp], userId, timestamp'
});

Здесь:

  • [userId+timestamp] — первичный ключ;
  • userId и timestamp — вспомогательные индексы.

Ограничения и особенности первичных ключей

Неизменяемость ключа

Первичный ключ в IndexedDB является неизменяемым после вставки. Попытка обновления ключа фактически требует удаления и повторного добавления записи.

Уникальность

Любая попытка вставить запись с уже существующим ключом приводит к ошибке ConstraintError.

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

Выбор ключа напрямую влияет на:

  • скорость поиска;
  • размер индексов;
  • эффективность диапазонных операций;
  • структуру B-Tree внутри хранилища.

Сравнение типов ключей

Тип ключа Генерация Гибкость Сценарии
Автоинкремент (++id) автоматическая низкая локальные сущности
Явный (string/number) внешняя высокая синхронизация, API
Составной ([a+b]) комбинированная средняя иерархические данные

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

Локальные сущности с автоинкрементом

db.version(1).stores({
  notes: '++id, title, body'
});

Модель ориентирована на автономное создание данных без внешних идентификаторов.


Синхронизируемые сущности с явным ключом

db.version(1).stores({
  users: 'id, name, updatedAt'
});

Используется идентификатор сервера, обеспечивающий консистентность между устройствами.


Иерархические данные с составным ключом

db.version(1).stores({
  messages: '[chatId+messageId], chatId, createdAt'
});

Такая структура позволяет:

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

Ключ как элемент модели данных

В Dexie.js первичный ключ перестаёт быть технической деталью и становится частью доменной модели. Его выбор определяет:

  • способ агрегации данных;
  • стратегию синхронизации;
  • характер запросов;
  • структуру индексов.

Проектирование ключей фактически задаёт архитектуру хранилища ещё до появления бизнес-логики.