db-identifiers и ansible/app-directories переписаны на формальный язык
- 7 и 9 правил соответственно, у каждого модальность и обоснование - условие выбора первичного ключа стало правилом с таблицей веток, на которые можно ссылаться из отступлений и механизации
This commit is contained in:
+109
-58
@@ -1,79 +1,130 @@
|
||||
---
|
||||
status: рекомендуемая
|
||||
---
|
||||
|
||||
# Идентификаторы сущностей
|
||||
|
||||
Как выбираются и как выглядят первичные ключи. Схема БД меняется тяжело,
|
||||
поэтому конвенция применяется **к новым таблицам**; существующие живут как
|
||||
есть и перечислены в отступлениях.
|
||||
Как выбираются и как выглядят первичные ключи сущностей. Форма записи —
|
||||
`common/language.md`.
|
||||
|
||||
## Условие применимости
|
||||
## Область действия
|
||||
|
||||
Вопрос задаётся **один раз на репозиторий**, а не по таблицам:
|
||||
Схема базы меняется тяжело: таблица не переезжает от того, что её
|
||||
потрогали. Поэтому правила распространяются на **новые таблицы**;
|
||||
существующие живут как есть и перечисляются в отступлениях, причём этот
|
||||
список постоянный, а не список задач на дочистку.
|
||||
|
||||
> Есть ли в приложении хотя бы одна сущность, которую адресуют извне — по
|
||||
> id из URL, запроса API или callback-данных?
|
||||
>
|
||||
> - **Да** → весь репозиторий на сортируемый строковый id, который
|
||||
> генерирует приложение (ULID), включая внутренние таблицы.
|
||||
> - **Ни одной** → автоинкремент, и этого достаточно.
|
||||
## Правила
|
||||
|
||||
Критерий — именно **адресация**: снаружи по этому id возвращаются к
|
||||
системе. Не «id виден в логе» — туда рано или поздно попадает любой
|
||||
идентификатор, и по такому критерию вторая ветка была бы недостижима.
|
||||
### R1. Вид первичного ключа выбирается один раз на репозиторий
|
||||
|
||||
Почему квантор репозиторный, а не потабличный: внутренние сущности имеют
|
||||
привычку становиться внешними, и тогда целочисленный id утекает в URL
|
||||
задним числом. Плюс одна ментальная модель дешевле, чем спор при заведении
|
||||
каждой таблицы.
|
||||
**ДОЛЖЕН.** Репозиторий отвечает на один вопрос и держит ответ для всех
|
||||
своих таблиц:
|
||||
|
||||
**Что не является смешиванием.** Запрет касается двух видов
|
||||
*сгенерированных суррогатных* ключей в одной базе. Естественные и составные
|
||||
ключи у таблиц-деталей — третья категория, они допустимы всегда (см. ниже).
|
||||
> Есть ли в приложении хотя бы одна сущность, которую адресуют **извне** —
|
||||
> по идентификатору из URL, запроса API или callback-данных?
|
||||
|
||||
| № | Ответ | Вид ключа |
|
||||
|---|---|---|
|
||||
| R1.1 | да, хотя бы одна | сортируемый строковый идентификатор, который генерирует приложение (ULID) — во **всех** таблицах, включая внутренние |
|
||||
| R1.2 | ни одной | автоинкремент |
|
||||
|
||||
**Почему.** Квантор репозиторный, а не потабличный, по двум причинам.
|
||||
Внутренние сущности имеют привычку становиться внешними — и тогда
|
||||
целочисленный идентификатор утекает в URL задним числом, а миграция ключа
|
||||
на живых данных стоит несопоставимо дороже, чем взять строковый сразу.
|
||||
Вторая причина дешевле, но важнее в быту: одна ментальная модель избавляет
|
||||
от спора при заведении каждой таблицы.
|
||||
|
||||
Критерий — именно **адресация**: снаружи по этому идентификатору
|
||||
возвращаются к системе. Не «виден в логе»: туда рано или поздно попадает
|
||||
любой идентификатор, и по такому критерию ветка R1.2 была бы недостижима.
|
||||
|
||||
Запрет смешивания касается двух видов **сгенерированных суррогатных**
|
||||
ключей. Естественные и составные ключи у таблиц-деталей (R6) — третья
|
||||
категория, они допустимы при любом ответе.
|
||||
|
||||
<!-- local:решение -->
|
||||
<!-- /local -->
|
||||
|
||||
## Если ULID
|
||||
### R2. При выборе R1.1 идентификатор генерирует приложение, а не база
|
||||
|
||||
- **PK — TEXT ULID** (26 символов Crockford base32), генерируется
|
||||
**приложением** в момент создания записи, а не БД.
|
||||
- Почему не UUID: UUIDv4 не сортируется по времени. UUIDv7 (RFC 9562)
|
||||
сортируется, и против него остаются два довода — 36 символов против 26 и
|
||||
дефисы: без них `grep` и двойной клик берут id целиком.
|
||||
- Сортировка по времени создания даёт `ORDER BY id` = хронология с
|
||||
точностью до миллисекунды. Внутри одной миллисекунды порядок произволен,
|
||||
если генератор не монотонный, — на хронологию событий это не влияет.
|
||||
- Глобальная уникальность across таблиц даёт побочный, но важный эффект:
|
||||
голый `grep` по id находит все записи сущности независимо от имени поля.
|
||||
- **Единая точка генерации и разбора.** Один модуль генерирует id и один
|
||||
разбирает; самодельных генераторов по коду нет.
|
||||
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
||||
|
||||
## Канонический вид и границы
|
||||
**Почему.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
|
||||
начатой операции, кладут в связанные записи одной транзакции и возвращают
|
||||
клиенту. Генерация на стороне базы вынуждает либо ждать `last_insert_rowid`
|
||||
и достраивать связи вторым проходом, либо иметь два источника истины о
|
||||
моменте создания.
|
||||
|
||||
- Генерим и храним id в **нижнем регистре**. Сравнение строк в БД обычно
|
||||
побайтовое, поэтому регистр — не косметика, а корректность. Спецификация
|
||||
ULID канонизирует верхний регистр, и библиотеки по умолчанию отдают
|
||||
именно его — нижний обеспечивает единая точка генерации, поэтому звать
|
||||
библиотеку мимо неё нельзя.
|
||||
- Любой пришедший снаружи id **обязательно** проходит разбор до запроса к
|
||||
БД: он валидирует формат и нормализует регистр.
|
||||
- Синтаксически невалидный id, которым **адресуют ресурс**, трактуем как
|
||||
несуществующую сущность (404), **без похода в БД**: это и дешевле, и
|
||||
убирает целый класс запросов с мусором. Невалидный id, пришедший из
|
||||
собственной формы или кнопки, — не «не найдено», а некорректный ввод: там
|
||||
это признак устаревшего интерфейса или бага, и маскировать его под 404
|
||||
значит терять диагностику.
|
||||
### R3. Генерация и разбор идентификаторов — в единственной точке
|
||||
|
||||
## Естественные и составные ключи — для деталей
|
||||
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
|
||||
Самодельных генераторов и парсеров в коде нет.
|
||||
|
||||
У таблиц-деталей и связей допустим естественный или составной ключ вместо
|
||||
сгенерированного, когда он есть по природе данных. Отдельный id там —
|
||||
мёртвый вес, который ещё и создаёт второй способ адресовать ту же строку.
|
||||
**Почему.** Нормализация регистра (R4) и проверка формата обязаны
|
||||
применяться ко всем идентификаторам без исключения. Любая вторая точка
|
||||
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
|
||||
проявится не там, где создан.
|
||||
|
||||
Прочие генерируемые идентификаторы (батчи, задания, корреляционные ключи) —
|
||||
через ту же единую точку: единый формат, сортируемость, корреляция в логах.
|
||||
### R4. Канонический вид — нижний регистр
|
||||
|
||||
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
|
||||
|
||||
**Почему.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
|
||||
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
|
||||
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (R3)
|
||||
разный регистр появится в базе сам собой.
|
||||
|
||||
### R5. Внешний идентификатор разбирается до обращения к базе
|
||||
|
||||
**ДОЛЖЕН.** Значение, пришедшее снаружи, проходит разбор и нормализацию
|
||||
раньше, чем по нему делается запрос. Реакция на неудачный разбор зависит от
|
||||
источника:
|
||||
|
||||
| № | Откуда пришёл | Разбор не удался → |
|
||||
|---|---|---|
|
||||
| R5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
||||
| R5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
||||
|
||||
**Почему.** Синтаксически невалидное значение не может соответствовать
|
||||
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
|
||||
границе, мы дёшево снимаем целый класс мусорного трафика.
|
||||
|
||||
Разделение R5.1 и R5.2 нужно, потому что источники значат разное. Мусор в
|
||||
URL — это чужая или протухшая ссылка, и «не найдено» описывает ситуацию
|
||||
точно. Мусор из собственной формы — это баг интерфейса или устаревший
|
||||
экран; ответ «не найдено» здесь скрывает дефект и лишает диагностики
|
||||
единственный момент, когда он заметен.
|
||||
|
||||
### R6. У таблиц-деталей допустим естественный или составной ключ
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
||||
сгенерированный идентификатор не заводится.
|
||||
|
||||
**Почему.** Суррогат поверх естественного ключа создаёт второй способ
|
||||
адресовать ту же строку — а значит, возможность рассинхрона между ними и
|
||||
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
|
||||
информации он не несёт.
|
||||
|
||||
### R7. Прочие генерируемые идентификаторы — через ту же точку
|
||||
|
||||
**ДОЛЖЕН.** Идентификаторы, не являющиеся первичными ключами (батч,
|
||||
задание, корреляционный ключ), порождаются тем же модулем (R3) и в том же
|
||||
формате.
|
||||
|
||||
**Почему.** Единый формат делает работающим главный побочный эффект
|
||||
строковых идентификаторов: `grep` по голому значению собирает все
|
||||
упоминания сущности в логах независимо от имени поля. Второй формат
|
||||
идентификаторов эту возможность отменяет ровно для тех записей, где она
|
||||
чаще всего нужна.
|
||||
|
||||
## Почему ULID, а не UUID
|
||||
|
||||
Ветка R1.1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
||||
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
|
||||
остаются два довода: 36 символов против 26 и дефисы, из-за которых
|
||||
идентификатор не берётся ни двойным кликом, ни `grep`-ом как одно слово.
|
||||
|
||||
Сортировка даёт `ORDER BY id` = хронология с точностью до миллисекунды;
|
||||
внутри одной миллисекунды порядок произволен, если генератор не монотонный,
|
||||
— на порядок событий это не влияет.
|
||||
|
||||
<!-- local:отступления -->
|
||||
<!-- /local -->
|
||||
|
||||
Reference in New Issue
Block a user