- сами конвенции переехали в conventions/, описательное — в корень: LANGUAGE.md (язык записи) и GUIDE.md (как ведут конвенции) - conv синхронизирует только conventions/, пути в origin даются относительно неё — раскладка копий в репозиториях не меняется
9.5 KiB
Идентификаторы сущностей
Как выбираются и как выглядят первичные ключи сущностей. Форма записи —
LANGUAGE.md.
Область действия
Схема базы меняется тяжело: таблица не переезжает от того, что её потрогали. Поэтому правила распространяются на новые таблицы; существующие живут как есть и перечисляются в отступлениях, причём этот список постоянный, а не список задач на дочистку.
Правила
R1. Вид первичного ключа выбирается один раз на репозиторий
ДОЛЖЕН. Репозиторий отвечает на один вопрос и держит ответ для всех своих таблиц:
Есть ли в приложении хотя бы одна сущность, которую адресуют извне — по идентификатору из URL, запроса API или callback-данных?
| № | Ответ | Вид ключа |
|---|---|---|
| R1.1 | да, хотя бы одна | сортируемый строковый идентификатор, который генерирует приложение (ULID) — во всех таблицах, включая внутренние |
| R1.2 | ни одной | автоинкремент |
Почему. Квантор репозиторный, а не потабличный, по двум причинам. Внутренние сущности имеют привычку становиться внешними — и тогда целочисленный идентификатор утекает в URL задним числом, а миграция ключа на живых данных стоит несопоставимо дороже, чем взять строковый сразу. Вторая причина дешевле, но важнее в быту: одна ментальная модель избавляет от спора при заведении каждой таблицы.
Критерий — именно адресация: снаружи по этому идентификатору возвращаются к системе. Не «виден в логе»: туда рано или поздно попадает любой идентификатор, и по такому критерию ветка R1.2 была бы недостижима.
Запрет смешивания касается двух видов сгенерированных суррогатных ключей. Естественные и составные ключи у таблиц-деталей (R6) — третья категория, они допустимы при любом ответе.
R2. При выборе R1.1 идентификатор генерирует приложение, а не база
ДОЛЖЕН. Значение ключа известно до вставки строки.
Почему. Идентификатор нужен раньше, чем база ответит: его пишут в лог
начатой операции, кладут в связанные записи одной транзакции и возвращают
клиенту. Генерация на стороне базы вынуждает либо ждать last_insert_rowid
и достраивать связи вторым проходом, либо иметь два источника истины о
моменте создания.
R3. Генерация и разбор идентификаторов — в единственной точке
ДОЛЖЕН. Один модуль порождает идентификаторы, он же их разбирает. Самодельных генераторов и парсеров в коде нет.
Почему. Нормализация регистра (R4) и проверка формата обязаны применяться ко всем идентификаторам без исключения. Любая вторая точка входа рано или поздно окажется той, где нормализацию забыли, — и дефект проявится не там, где создан.
R4. Канонический вид — нижний регистр
ДОЛЖЕН. Идентификаторы порождаются и хранятся в нижнем регистре.
Почему. Сравнение строк в базе обычно побайтовое, поэтому регистр — не косметика, а корректность поиска. Спецификация ULID канонизирует верхний регистр, и библиотеки по умолчанию отдают именно его: без единой точки (R3) разный регистр появится в базе сам собой.
R5. Внешний идентификатор разбирается до обращения к базе
ДОЛЖЕН. Значение, пришедшее снаружи, проходит разбор и нормализацию раньше, чем по нему делается запрос. Реакция на неудачный разбор зависит от источника:
| № | Откуда пришёл | Разбор не удался → |
|---|---|---|
| R5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
| R5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
Почему. Синтаксически невалидное значение не может соответствовать записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на границе, мы дёшево снимаем целый класс мусорного трафика.
Разделение R5.1 и R5.2 нужно, потому что источники значат разное. Мусор в URL — это чужая или протухшая ссылка, и «не найдено» описывает ситуацию точно. Мусор из собственной формы — это баг интерфейса или устаревший экран; ответ «не найдено» здесь скрывает дефект и лишает диагностики единственный момент, когда он заметен.
R6. У таблиц-деталей допустим естественный или составной ключ
ДОПУСКАЕТСЯ. Когда ключ есть по природе данных, отдельный сгенерированный идентификатор не заводится.
Почему. Суррогат поверх естественного ключа создаёт второй способ адресовать ту же строку — а значит, возможность рассинхрона между ними и лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной информации он не несёт.
R7. Прочие генерируемые идентификаторы — через ту же точку
ДОЛЖЕН. Идентификаторы, не являющиеся первичными ключами (батч, задание, корреляционный ключ), порождаются тем же модулем (R3) и в том же формате.
Почему. Единый формат делает работающим главный побочный эффект
строковых идентификаторов: grep по голому значению собирает все
упоминания сущности в логах независимо от имени поля. Второй формат
идентификаторов эту возможность отменяет ровно для тех записей, где она
чаще всего нужна.
Почему ULID, а не UUID
Ветка R1.1 требует сортируемый строковый идентификатор. UUIDv4 не
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
остаются два довода: 36 символов против 26 и дефисы, из-за которых
идентификатор не берётся ни двойным кликом, ни grep-ом как одно слово.
Сортировка даёт ORDER BY id = хронология с точностью до миллисекунды;
внутри одной миллисекунды порядок произволен, если генератор не монотонный,
— на порядок событий это не влияет.
Связано
arch/time.md— метки времени тоже генерирует приложение, а не схема.