- пятая, необязательная часть правила: код парой «плохо → хорошо» после обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии языка во всех тринадцати файлах - сказано, чем примеры не являются: требований в блоке нет, дословным сниппетом он не служит, при расхождении с нормой правят пример - READING.md обновлён по META-30, в машинные проверки добавлен порядок блоков, в читательские — что примеры норму не расширяют
10 KiB
topic, prefix
| topic | prefix |
|---|---|
| db-identifiers | KEYS |
Идентификаторы сущностей
Как выбираются и как выглядят первичные ключи сущностей.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Область действия
Схема базы меняется тяжело: таблица не переезжает от того, что её потрогали. Поэтому правила распространяются на новые таблицы; существующие живут как есть и перечисляются в отступлениях, причём этот список постоянный, а не список задач на дочистку. Целочисленные ключи существующих приложений — именно такой случай: они не мигрируют, и правила их работы описаны в конвенции схемы, а не здесь.
Правила
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— метки времени тоже генерирует приложение, а не схема.