заведён канон общих конвенций для личных проектов

- 13 конвенций по осям arch / lang / stack / common; репозитории берут
  оттуда копии в свой docs/conventions/ и коммитят их у себя
- conv — синхронизация копий: add / status / diff / pull / push, локальные
  регионы исключены из сравнения, поэтому расхождение не даёт шума
This commit is contained in:
av
2026-07-25 18:18:18 +03:00
commit 4a59c71737
15 changed files with 2142 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
---
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 -->