Files
dev-conventions/conventions/arch/db-identifiers.md
T
av 59a1c23f55 язык: заведён блок ПРИМЕРЫ
- пятая, необязательная часть правила: код парой «плохо → хорошо» после
  обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии
  языка во всех тринадцати файлах
- сказано, чем примеры не являются: требований в блоке нет, дословным
  сниппетом он не служит, при расхождении с нормой правят пример
- READING.md обновлён по META-30, в машинные проверки добавлен порядок
  блоков, в читательские — что примеры норму не расширяют
2026-07-26 16:09:58 +03:00

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 — метки времени тоже генерирует приложение, а не схема.