Перейти к содержанию

Генерация идентификаторов

Каждый курс имеет числовой ключ, а приложение обычно создаёт запись без готового значения этого ключа. База должна выдать новое значение так, чтобы одновременно работающие клиенты не выбрали одно и то же число. Для нашего каталога подходит колонка identity. Однако последовательность идентификаторов не является обещанием непрерывной нумерации видимых строк.

Рассмотрим PostgreSQL 17.11 и снимок catalog-db/lesson-06 из архива серии. SQL не выполнялся. В снимке сначала отдельно восстанавливается исходная схема с шестью курсами, затем пример создаёт временные строки внутри транзакций и откатывает их. Откат строк не возвращает уже выданные значения последовательности, поэтому повтор номера из текста требует нового учебного reset.

Identity и первичный ключ

В определении таблицы используется следующий фрагмент:

id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY

Identity связывает колонку с генератором значений. PRIMARY KEY закрепляет уникальность и обязательность ключа. Это два связанных, но разных механизма: генерация помогает получить значение, ограничение не позволяет сохранить неподходящую строку. Свойства identity описаны в документации PostgreSQL.

Слово ALWAYS означает, что обычная вставка не должна самостоятельно подменять выданный идентификатор. Для специальных операций импорта существуют явные механизмы переопределения, однако начальное приложение ими не пользуется. Когда создаётся черновик курса, форма передаёт название и тему, а не выбирает свободный ключ вручную.

Почему не взять max(id) + 1? Представьте два клиента, читающих одно максимальное значение шесть. Оба вычислят семь. Первый сохранит запись, второй столкнётся с конфликтом. Проблема возникает между чтением и записью. Генератор обслуживает выдачу значений в базе, а не заставляет каждый клиент угадывать состояние других запросов.

Такое объяснение не означает, что число само по себе защищает объект. По идентификатору всё ещё можно запросить чужую запись, если приложение не проверяет доступ. Уникальность ключа и авторизация являются разными правилами. В каталоге примера нет пользовательских личных данных, но этот смысл полезно понимать до добавления реальных аккаунтов.

Получение выданного значения

Создадим временный черновик без поля id и прочитаем результат через RETURNING:

BEGIN;
INSERT INTO catalog.courses
(topic_id, slug, title, planned_lessons, price)
VALUES (1, 'identity-demo', 'Учебный черновик', 8, 0)
RETURNING id, slug, status;
ROLLBACK;

После свежего reset без других операций ожидается ключ семь, код identity-demo и статус draft. Время публикации остаётся отсутствующим, что согласовано с ограничением. Значение статуса взялось из значения по умолчанию. После отката самой строки в таблице уже нет, поэтому чтение по этому slug не должно найти курс.

Команда возвращает ключ именно созданной строки. Не нужно искать максимальный id после вставки: к этому моменту другой клиент мог создать ещё один курс. Показ результата в ответе операции сохраняет связь между вашим действием и полученным объектом. Подробно применение RETURNING разберём в следующем уроке.

Теперь повторим похожую временную вставку с другим кодом. После первой операции генератор уже выдавал семь. Поэтому второй ожидаемый ключ — восемь, хотя курс с ключом семь не сохранился. Такой пропуск является нормальным следствием разделения строк и выдачи значений. Он не говорит о скрытой записи, которую нужно немедленно искать или восстанавливать.

BEGIN;
INSERT INTO catalog.courses
(topic_id, slug, title, planned_lessons, price)
VALUES (1, 'identity-demo-next', 'Другой черновик', 8, 0)
RETURNING id;
ROLLBACK;

Если выполнить только второй фрагмент сразу после reset, ожидается семь, а не восемь. Поэтому результат определяется не номером блока на странице, а историей обращений к генератору. В lesson.sql оба фрагмента стоят последовательно. При выполнении снимка ещё раз без reset номера продолжат двигаться, хотя итоговый набор строк будет прежним.

Пропуски и порядок

По правилам последовательностей PostgreSQL выданное значение не возвращается при откате. Пропуски могут появиться и при других неудачных операциях. Полагаться на непрерывность identity для бухгалтерского номера или точного количества объектов нельзя. Если нужен юридически значимый номер документа, это отдельная предметная модель с собственными требованиями.

Порядок числовых ключей также не заменяет момент публикации. Курс может быть создан раньше, а опубликован позже. В учебном наборе идентификатор три и пять имеют одинаковое время публикации, а курс четыре вообще остаётся черновиком. Если сортировать ленту только по ключу, порядок будет описывать выбранный технический признак, а не обещанную читателю свежесть.

Для оглавления уроков используется position, поскольку редактор может вставить главу в середину серии. Перенумерация учебного порядка не должна менять первичные ключи уроков. Тогда ссылки приложения на конкретный объект сохраняются при изменении расположения. Это ещё одна причина отличать идентичность от номера, который видит человек.

Можно ли использовать UUID вместо большого целого? Да, когда договор приложения этого требует: например, идентификатор создаётся вне центральной базы. Но выбор влияет на представление, передачу и индексы, а не отменяет необходимость уникальности. В этой серии единый сервер выдаёт ключи, поэтому дополнительную задачу распределённой генерации не вводим.

Не перестраиваем ключи ради красоты

После удаления строки не нужно уменьшать остальные ключи, чтобы снова получить ровную последовательность. Уроки ссылаются на курс через course_id; внешние системы могли сохранить его идентификатор. Массовое исправление чисел ради видимой непрерывности затронуло бы смысл связей. Числовой ключ удобен именно тем, что остаётся стабильным для существующего объекта.

В начальном reset таблицы создаются заново, и последовательности начинаются с известного состояния. Это редакционный способ получить воспроизводимый учебный набор, а не способ обычного обслуживания каталога. На живой базе сбрасывать генератор без проверки уже сохранённых ключей можно получить конфликт при следующей вставке.

Выдача числа и завершение изменения

Представьте два редакторских клиента. Первый получил новый ключ и ещё не закончил свою транзакцию. Второй получил следующий ключ и успел сохранить запись раньше. Числовая последовательность выдачи не обязана совпадать с порядком завершения изменений. Поэтому сортировка по id не является универсальным журналом подтверждённых действий.

Отдельно рассмотрим счётчик видимых объектов. Если в таблице шесть строк и максимальный ключ равен восьми, это не даёт точного количества удалённых курсов. Пропуски могли возникнуть при откате, неудачной попытке или другой истории генератора. Для числа существующих объектов используется подсчёт строк с нужным предметным условием, а не разность крайних ключей.

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

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

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

Оглавление курса.