- метка обоснования пишется заглавными и вошла в словарь набора: скелет правила теперь целиком из ключевых слов, а не смесь `**ДОЛЖЕН.**` и `**Почему.**`; в переводе на другой язык метка меняется как остальные слова (ПОЧЕМУ / WHY), 235 вхождений заменены - метки правила выделены из шкалы в отдельный перечень: ПОЧЕМУ и МЕХАНИЗИРОВАНО обязательности не задают, а размечают части, и стандартом не даются ни в одном языке — раньше МЕХАНИЗИРОВАНО висело строкой в таблице модальности - версия языка поднята до 2, потому что изменение формы меняет чтение уже написанного текста; строка о версии в двенадцати конвенциях перечисляет теперь и метки, а служебные слова сценария в неё по-прежнему не входят
140 lines
10 KiB
Markdown
140 lines
10 KiB
Markdown
---
|
||
prefix: KEYS
|
||
---
|
||
|
||
# Идентификаторы сущностей
|
||
|
||
Как выбираются и как выглядят первичные ключи сущностей.
|
||
|
||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2 —
|
||
тогда и только тогда, когда написаны заглавными.
|
||
|
||
## Область действия
|
||
|
||
Схема базы меняется тяжело: таблица не переезжает от того, что её
|
||
потрогали. Поэтому правила распространяются на **новые таблицы**;
|
||
существующие живут как есть и перечисляются в отступлениях, причём этот
|
||
список постоянный, а не список задач на дочистку. Целочисленные ключи
|
||
существующих приложений — именно такой случай: они не мигрируют, и правила
|
||
их работы описаны в конвенции схемы, а не здесь.
|
||
|
||
## Правила
|
||
|
||
### KEYS-1. Первичный ключ новой сущности — ULID
|
||
|
||
**ДОЛЖЕН.** Новая сущность получает сортируемый строковый идентификатор,
|
||
который порождает приложение, — во **всех** таблицах, включая те, что
|
||
снаружи не адресуются.
|
||
|
||
**ПОЧЕМУ.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
|
||
сущность снаружи не адресуют, ей хватит целого числа». Внутренние сущности
|
||
имеют привычку становиться внешними — и тогда целочисленный идентификатор
|
||
утекает в URL задним числом, а миграция ключа на живых данных стоит
|
||
несопоставимо дороже, чем взять строковый сразу. Заранее отличить те, с
|
||
кем это случится, не получается: если бы получалось, они бы уже назывались
|
||
внешними.
|
||
|
||
Второй довод дешевле, но важнее в быту: одна ментальная модель избавляет от
|
||
спора при заведении каждой таблицы и делает идентификатор **глобальным** —
|
||
уникальным across таблиц, а не только внутри своей. На этом держится
|
||
корреляция по логам (KEYS-7).
|
||
|
||
Правило про **сгенерированные суррогатные** ключи. Естественные и составные
|
||
ключи у таблиц-деталей (KEYS-6) — третья категория, они допустимы всегда.
|
||
|
||
### KEYS-2. Идентификатор генерирует приложение, а не база
|
||
|
||
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
||
|
||
**ПОЧЕМУ.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
|
||
начатой операции, кладут в связанные записи одной транзакции и возвращают
|
||
клиенту. Генерация на стороне базы вынуждает либо ждать `last_insert_rowid`
|
||
и достраивать связи вторым проходом, либо иметь два источника истины о
|
||
моменте создания.
|
||
|
||
### KEYS-3. Генерация и разбор идентификаторов — в единственной точке
|
||
|
||
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
|
||
Самодельных генераторов и парсеров в коде нет.
|
||
|
||
**ПОЧЕМУ.** Нормализация регистра (KEYS-4) и проверка формата обязаны
|
||
применяться ко всем идентификаторам без исключения. Любая вторая точка
|
||
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
|
||
проявится не там, где создан.
|
||
|
||
### KEYS-4. Канонический вид — нижний регистр
|
||
|
||
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
|
||
|
||
**ПОЧЕМУ.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
|
||
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
|
||
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (KEYS-3)
|
||
разный регистр появится в базе сам собой.
|
||
|
||
### KEYS-5. Внешний идентификатор разбирается до обращения к базе
|
||
|
||
**ДОЛЖЕН.** Значение, пришедшее снаружи, проходит разбор и нормализацию
|
||
раньше, чем по нему делается запрос. Реакция на неудачный разбор зависит от
|
||
источника:
|
||
|
||
| № | Откуда пришёл | Разбор не удался → |
|
||
|---|---|---|
|
||
| KEYS-5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
||
| KEYS-5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
||
|
||
**ПОЧЕМУ.** Синтаксически невалидное значение не может соответствовать
|
||
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
|
||
границе, мы дёшево снимаем целый класс мусорного трафика.
|
||
|
||
Разделение KEYS-5.1 и KEYS-5.2 нужно, потому что источники значат разное.
|
||
Мусор в URL — это чужая или протухшая ссылка, и «не найдено» описывает
|
||
ситуацию точно. Мусор из собственной формы — это баг интерфейса или
|
||
устаревший экран; ответ «не найдено» здесь скрывает дефект и лишает
|
||
диагностики единственный момент, когда он заметен.
|
||
|
||
Таблица перечисляет **внешние** источники — те, откуда значение приходит
|
||
вместе с запросом, и правило говорит, что отдать в ответ. Идентификатор из
|
||
конфигурации, из собственной базы или из фикстуры сюда не относится: он
|
||
ничего не отдаёт наружу, а его невалидность означает, что сломано у нас.
|
||
Формат идентификатора в конфигурации проверяется на старте
|
||
(`CONF-18`), невалидное значение в собственной базе — нарушенный
|
||
инвариант единой точки (KEYS-3).
|
||
|
||
### KEYS-6. У таблиц-деталей допустим естественный или составной ключ
|
||
|
||
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
||
сгенерированный идентификатор не заводится.
|
||
|
||
**ПОЧЕМУ.** Суррогат поверх естественного ключа создаёт второй способ
|
||
адресовать ту же строку — а значит, возможность рассинхрона между ними и
|
||
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
|
||
информации он не несёт.
|
||
|
||
### KEYS-7. Прочие генерируемые идентификаторы — через ту же точку
|
||
|
||
**ДОЛЖЕН.** Идентификаторы, не являющиеся первичными ключами (батч,
|
||
задание, корреляционный ключ), порождаются тем же модулем (KEYS-3) и в том же
|
||
формате.
|
||
|
||
**ПОЧЕМУ.** Единый формат делает работающим главный побочный эффект
|
||
строковых идентификаторов: `grep` по голому значению собирает все
|
||
упоминания сущности в логах независимо от имени поля. Второй формат
|
||
идентификаторов эту возможность отменяет ровно для тех записей, где она
|
||
чаще всего нужна.
|
||
|
||
## Почему ULID, а не UUID
|
||
|
||
KEYS-1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
||
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
|
||
остаются два довода: 36 символов против 26 и дефисы, из-за которых
|
||
идентификатор не берётся ни двойным кликом, ни `grep`-ом как одно слово.
|
||
|
||
Сортировка даёт `ORDER BY id` = хронология с точностью до миллисекунды;
|
||
внутри одной миллисекунды порядок произволен, если генератор не монотонный,
|
||
— на порядок событий это не влияет.
|
||
|
||
## Связано
|
||
|
||
- конвенция `time` — метки времени тоже генерирует приложение, а не схема.
|