--- status: рекомендуемая --- # Идентификаторы сущностей Как выбираются и как выглядят первичные ключи. Схема БД меняется тяжело, поэтому конвенция применяется **к новым таблицам**; существующие живут как есть и перечислены в отступлениях. ## Условие применимости Вопрос задаётся **один раз на репозиторий**, а не по таблицам: > Есть ли в приложении хотя бы одна сущность, которую адресуют извне — по > id из URL, запроса API или callback-данных? > > - **Да** → весь репозиторий на сортируемый строковый id, который > генерирует приложение (ULID), включая внутренние таблицы. > - **Ни одной** → автоинкремент, и этого достаточно. Критерий — именно **адресация**: снаружи по этому id возвращаются к системе. Не «id виден в логе» — туда рано или поздно попадает любой идентификатор, и по такому критерию вторая ветка была бы недостижима. Почему квантор репозиторный, а не потабличный: внутренние сущности имеют привычку становиться внешними, и тогда целочисленный id утекает в URL задним числом. Плюс одна ментальная модель дешевле, чем спор при заведении каждой таблицы. **Что не является смешиванием.** Запрет касается двух видов *сгенерированных суррогатных* ключей в одной базе. Естественные и составные ключи у таблиц-деталей — третья категория, они допустимы всегда (см. ниже). ## Если ULID - **PK — TEXT ULID** (26 символов Crockford base32), генерируется **приложением** в момент создания записи, а не БД. - Почему не UUID: UUIDv4 не сортируется по времени. UUIDv7 (RFC 9562) сортируется, и против него остаются два довода — 36 символов против 26 и дефисы: без них `grep` и двойной клик берут id целиком. - Сортировка по времени создания даёт `ORDER BY id` = хронология с точностью до миллисекунды. Внутри одной миллисекунды порядок произволен, если генератор не монотонный, — на хронологию событий это не влияет. - Глобальная уникальность across таблиц даёт побочный, но важный эффект: голый `grep` по id находит все записи сущности независимо от имени поля. - **Единая точка генерации и разбора.** Один модуль генерирует id и один разбирает; самодельных генераторов по коду нет. ## Канонический вид и границы - Генерим и храним id в **нижнем регистре**. Сравнение строк в БД обычно побайтовое, поэтому регистр — не косметика, а корректность. Спецификация ULID канонизирует верхний регистр, и библиотеки по умолчанию отдают именно его — нижний обеспечивает единая точка генерации, поэтому звать библиотеку мимо неё нельзя. - Любой пришедший снаружи id **обязательно** проходит разбор до запроса к БД: он валидирует формат и нормализует регистр. - Синтаксически невалидный id, которым **адресуют ресурс**, трактуем как несуществующую сущность (404), **без похода в БД**: это и дешевле, и убирает целый класс запросов с мусором. Невалидный id, пришедший из собственной формы или кнопки, — не «не найдено», а некорректный ввод: там это признак устаревшего интерфейса или бага, и маскировать его под 404 значит терять диагностику. ## Естественные и составные ключи — для деталей У таблиц-деталей и связей допустим естественный или составной ключ вместо сгенерированного, когда он есть по природе данных. Отдельный id там — мёртвый вес, который ещё и создаёт второй способ адресовать ту же строку. Прочие генерируемые идентификаторы (батчи, задания, корреляционные ключи) — через ту же единую точку: единый формат, сортируемость, корреляция в логах. ## Связано - `arch/time.md` — метки времени тоже генерирует приложение, а не схема.