Files
dev-conventions/conventions/arch/db-identifiers.md
T
av c1cb240540 темы: определение, объявление в шапке и манифест набора
- тема — набор правил об одном фокусе разработки, имя латиницей (нижний
  kebab-case рекомендуется, годится любой идентификатор, пригодный для имени
  файла); определение в LANGUAGE.md и README.md
- заведены META-28 и META-29: тема объявляется в шапке (`topic:`), стоит в
  манифесте набора и не переиспользуется; `topic:` добавлен во все 12 файлов
- prefixes.toml и topics.toml слиты в manifest.toml — манифест набора против
  манифеста подключения `.conventions.toml`, разделы topics/prefixes с live
  и retired
2026-07-26 15:27:55 +03:00

141 lines
10 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.
---
topic: db-identifiers
prefix: 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` — метки времени тоже генерирует приложение, а не схема.