Files
dev-conventions/arch/db-identifiers.md
T
av 4a59c71737 заведён канон общих конвенций для личных проектов
- 13 конвенций по осям arch / lang / stack / common; репозитории берут
  оттуда копии в свой docs/conventions/ и коммитят их у себя
- conv — синхронизация копий: add / status / diff / pull / push, локальные
  регионы исключены из сравнения, поэтому расхождение не даёт шума
2026-07-25 18:18:18 +03:00

6.1 KiB
Raw Blame History

status
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 — метки времени тоже генерирует приложение, а не схема.