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

87 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: рекомендуемая
---
# Идентификаторы сущностей
Как выбираются и как выглядят первичные ключи. Схема БД меняется тяжело,
поэтому конвенция применяется **к новым таблицам**; существующие живут как
есть и перечислены в отступлениях.
## Условие применимости
Вопрос задаётся **один раз на репозиторий**, а не по таблицам:
> Есть ли в приложении хотя бы одна сущность, которую адресуют извне — по
> id из URL, запроса API или callback-данных?
>
> - **Да** → весь репозиторий на сортируемый строковый id, который
> генерирует приложение (ULID), включая внутренние таблицы.
> - **Ни одной** → автоинкремент, и этого достаточно.
Критерий — именно **адресация**: снаружи по этому id возвращаются к
системе. Не «id виден в логе» — туда рано или поздно попадает любой
идентификатор, и по такому критерию вторая ветка была бы недостижима.
Почему квантор репозиторный, а не потабличный: внутренние сущности имеют
привычку становиться внешними, и тогда целочисленный id утекает в URL
задним числом. Плюс одна ментальная модель дешевле, чем спор при заведении
каждой таблицы.
**Что не является смешиванием.** Запрет касается двух видов
*сгенерированных суррогатных* ключей в одной базе. Естественные и составные
ключи у таблиц-деталей — третья категория, они допустимы всегда (см. ниже).
<!-- local:решение -->
<!-- /local -->
## Если 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 там —
мёртвый вес, который ещё и создаёт второй способ адресовать ту же строку.
Прочие генерируемые идентификаторы (батчи, задания, корреляционные ключи) —
через ту же единую точку: единый формат, сортируемость, корреляция в логах.
<!-- local:отступления -->
<!-- /local -->
## Связано
- `arch/time.md` — метки времени тоже генерирует приложение, а не схема.
<!-- local:связано -->
<!-- /local -->