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