идентификатор новой сущности — всегда ULID
- R1 больше не ветвится по признаку внешней адресуемости: заранее отличить внутренние сущности, которые станут внешними, невозможно - целочисленный ключ остаётся у существующих схем и идёт вместе с AUTOINCREMENT — переиспользованный rowid молча наводит протухшую ссылку на другую строку (db-schema R12) - таблица R5 ограничена внешними источниками, регион «решение» убран
This commit is contained in:
@@ -8,42 +8,35 @@
|
|||||||
Схема базы меняется тяжело: таблица не переезжает от того, что её
|
Схема базы меняется тяжело: таблица не переезжает от того, что её
|
||||||
потрогали. Поэтому правила распространяются на **новые таблицы**;
|
потрогали. Поэтому правила распространяются на **новые таблицы**;
|
||||||
существующие живут как есть и перечисляются в отступлениях, причём этот
|
существующие живут как есть и перечисляются в отступлениях, причём этот
|
||||||
список постоянный, а не список задач на дочистку.
|
список постоянный, а не список задач на дочистку. Целочисленные ключи
|
||||||
|
существующих приложений — именно такой случай: они не мигрируют, и правила
|
||||||
|
их работы описаны в конвенции схемы, а не здесь.
|
||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
|
|
||||||
### R1. Вид первичного ключа выбирается один раз на репозиторий
|
### R1. Первичный ключ новой сущности — ULID
|
||||||
|
|
||||||
**ДОЛЖЕН.** Репозиторий отвечает на один вопрос и держит ответ для всех
|
**ДОЛЖЕН.** Новая сущность получает сортируемый строковый идентификатор,
|
||||||
своих таблиц:
|
который порождает приложение, — во **всех** таблицах, включая те, что
|
||||||
|
снаружи не адресуются.
|
||||||
|
|
||||||
> Есть ли в приложении хотя бы одна сущность, которую адресуют **извне** —
|
**Почему.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
|
||||||
> по идентификатору из URL, запроса API или callback-данных?
|
сущность снаружи не адресуют, ей хватит целого числа». Внутренние сущности
|
||||||
|
имеют привычку становиться внешними — и тогда целочисленный идентификатор
|
||||||
|
утекает в URL задним числом, а миграция ключа на живых данных стоит
|
||||||
|
несопоставимо дороже, чем взять строковый сразу. Заранее отличить те, с
|
||||||
|
кем это случится, не получается: если бы получалось, они бы уже назывались
|
||||||
|
внешними.
|
||||||
|
|
||||||
| № | Ответ | Вид ключа |
|
Второй довод дешевле, но важнее в быту: одна ментальная модель избавляет от
|
||||||
|---|---|---|
|
спора при заведении каждой таблицы и делает идентификатор **глобальным** —
|
||||||
| R1.1 | да, хотя бы одна | сортируемый строковый идентификатор, который генерирует приложение (ULID) — во **всех** таблицах, включая внутренние |
|
уникальным across таблиц, а не только внутри своей. На этом держится
|
||||||
| R1.2 | ни одной | автоинкремент |
|
корреляция по логам (R7).
|
||||||
|
|
||||||
**Почему.** Квантор репозиторный, а не потабличный, по двум причинам.
|
Правило про **сгенерированные суррогатные** ключи. Естественные и составные
|
||||||
Внутренние сущности имеют привычку становиться внешними — и тогда
|
ключи у таблиц-деталей (R6) — третья категория, они допустимы всегда.
|
||||||
целочисленный идентификатор утекает в URL задним числом, а миграция ключа
|
|
||||||
на живых данных стоит несопоставимо дороже, чем взять строковый сразу.
|
|
||||||
Вторая причина дешевле, но важнее в быту: одна ментальная модель избавляет
|
|
||||||
от спора при заведении каждой таблицы.
|
|
||||||
|
|
||||||
Критерий — именно **адресация**: снаружи по этому идентификатору
|
### R2. Идентификатор генерирует приложение, а не база
|
||||||
возвращаются к системе. Не «виден в логе»: туда рано или поздно попадает
|
|
||||||
любой идентификатор, и по такому критерию ветка R1.2 была бы недостижима.
|
|
||||||
|
|
||||||
Запрет смешивания касается двух видов **сгенерированных суррогатных**
|
|
||||||
ключей. Естественные и составные ключи у таблиц-деталей (R6) — третья
|
|
||||||
категория, они допустимы при любом ответе.
|
|
||||||
|
|
||||||
<!-- local:решение -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
### R2. При выборе R1.1 идентификатор генерирует приложение, а не база
|
|
||||||
|
|
||||||
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
||||||
|
|
||||||
@@ -93,6 +86,14 @@ URL — это чужая или протухшая ссылка, и «не на
|
|||||||
экран; ответ «не найдено» здесь скрывает дефект и лишает диагностики
|
экран; ответ «не найдено» здесь скрывает дефект и лишает диагностики
|
||||||
единственный момент, когда он заметен.
|
единственный момент, когда он заметен.
|
||||||
|
|
||||||
|
Таблица перечисляет **внешние** источники — те, откуда значение приходит
|
||||||
|
вместе с запросом, и правило говорит, что отдать в ответ. Идентификатор из
|
||||||
|
конфигурации, из собственной базы или из фикстуры сюда не относится: он
|
||||||
|
ничего не отдаёт наружу, а его невалидность означает, что сломано у нас.
|
||||||
|
Формат идентификатора в конфигурации проверяется на старте
|
||||||
|
(`arch/config.md` R18), невалидное значение в собственной базе — нарушенный
|
||||||
|
инвариант единой точки (R3).
|
||||||
|
|
||||||
### R6. У таблиц-деталей допустим естественный или составной ключ
|
### R6. У таблиц-деталей допустим естественный или составной ключ
|
||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
||||||
@@ -117,7 +118,7 @@ URL — это чужая или протухшая ссылка, и «не на
|
|||||||
|
|
||||||
## Почему ULID, а не UUID
|
## Почему ULID, а не UUID
|
||||||
|
|
||||||
Ветка R1.1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
R1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
||||||
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
|
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
|
||||||
остаются два довода: 36 символов против 26 и дефисы, из-за которых
|
остаются два довода: 36 символов против 26 и дефисы, из-за которых
|
||||||
идентификатор не берётся ни двойным кликом, ни `grep`-ом как одно слово.
|
идентификатор не берётся ни двойным кликом, ни `grep`-ом как одно слово.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ extends: arch/db-identifiers.md
|
|||||||
|
|
||||||
# Идентификаторы: реализация на Go
|
# Идентификаторы: реализация на Go
|
||||||
|
|
||||||
Как `arch/db-identifiers.md` выглядит в Go-приложении, выбравшем ULID
|
Как `arch/db-identifiers.md` выглядит в Go-приложении. Форма записи —
|
||||||
(ветка `arch/db-identifiers.md` R1.1). Форма записи — `LANGUAGE.md`.
|
`LANGUAGE.md`.
|
||||||
|
|
||||||
Единая точка из `arch/db-identifiers.md` R3 — пакет `internal/ident`: он
|
Единая точка из `arch/db-identifiers.md` R3 — пакет `internal/ident`: он
|
||||||
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
|
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
|
||||||
|
|||||||
@@ -152,32 +152,33 @@ down останавливает сразу и заставляет пересо
|
|||||||
типа. Такой дефект не падает, не виден в логе и переживает тесты, которые
|
типа. Такой дефект не падает, не виден в логе и переживает тесты, которые
|
||||||
проверяют, что список не пуст.
|
проверяют, что список не пуст.
|
||||||
|
|
||||||
### R11. Вид первичного ключа задаёт `arch/db-identifiers.md`
|
### R11. Первичный ключ новой таблицы — TEXT ULID
|
||||||
|
|
||||||
**ДОЛЖЕН.** В репозитории, подписанном на `arch/db-identifiers.md`, вид
|
**ДОЛЖЕН.** Колонка ключа объявляется как `TEXT`, значение приходит из
|
||||||
ключа выбирается по её R1, и `AUTOINCREMENT` в миграции не пишется.
|
приложения (`arch/db-identifiers.md` R1, R2).
|
||||||
|
|
||||||
**Почему.** Вопрос о виде ключа решается один раз на репозиторий
|
**Почему.** Здесь конвенция схемы ничего не решает — она реализует решение,
|
||||||
(`arch/db-identifiers.md` R1). Повторив здесь его ветвление, мы завели бы
|
принятое в `arch/db-identifiers.md`. Повторить там ветвление или условие
|
||||||
второй источник правды, и соседние таблицы разъехались бы по разным
|
значило бы завести второй источник правды, и соседние таблицы разъехались бы
|
||||||
ответам на один и тот же вопрос.
|
по разным ответам на один вопрос.
|
||||||
|
|
||||||
`AUTOINCREMENT` не нужен ни в одной из веток R1. Строкового ключа он не
|
`AUTOINCREMENT` в такой таблице невозможен: SQLite разрешает его только на
|
||||||
касается вовсе, а целочисленному даёт единственную гарантию — что значение
|
`INTEGER PRIMARY KEY`. То есть для новых таблиц запрещать нечего.
|
||||||
rowid не будет переиспользовано после удаления строки, — ценой служебной
|
|
||||||
таблицы `sqlite_sequence` и записи в неё на каждой вставке. Гарантия эта
|
|
||||||
имеет смысл, только если старые идентификаторы живут где-то вне базы.
|
|
||||||
|
|
||||||
### R12. Вне `arch/db-identifiers.md` первичный ключ — автоинкремент
|
### R12. Целочисленный ключ идёт вместе с `AUTOINCREMENT`
|
||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** Репозиторий, не подписанный на `arch/db-identifiers.md`,
|
**ДОЛЖЕН.** Там, где первичный ключ всё-таки целочисленный — существующая
|
||||||
берёт целочисленный автоинкрементный ключ.
|
схема, миграция легаси-таблицы, — он объявляется с `AUTOINCREMENT`.
|
||||||
|
|
||||||
**Почему.** Явное разрешение нужно, чтобы R11 не читался как требование
|
**Почему.** Без него SQLite выдаёт rowid как `max(rowid)+1`, поэтому после
|
||||||
подписаться на `arch/db-identifiers.md`. Выбор вида ключа — решение уровня
|
удаления последней строки номер переиспользуется. Протухшая ссылка на
|
||||||
репозитория, и конвенция про типы колонок его за репозиторий не принимает;
|
удалённую запись — из закладки, из чужой таблицы, из старого лога — молча
|
||||||
приложению, сущности которого не адресуют снаружи, целочисленный ключ
|
наводится на другую сущность и возвращает правдоподобный, но чужой ответ.
|
||||||
ничего не стоит.
|
Обнаружить это по данным нельзя: обе строки валидны.
|
||||||
|
|
||||||
|
Цена — служебная таблица `sqlite_sequence` и запись в неё на каждой
|
||||||
|
вставке — против этого пренебрежима. Для новых таблиц вопрос не возникает:
|
||||||
|
там ключ строковый (R11).
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
<!-- local:механизировано -->
|
||||||
<!-- /local -->
|
<!-- /local -->
|
||||||
|
|||||||
Reference in New Issue
Block a user