- 11 файлов разобраны на нумерованные правила: 220 правил в каноне, у каждого модальность и обязательный блок «Почему» - классифицирующие места оформлены таблицами, файловый статус снят отовсюду, локальные регионы сохранены под прежними именами
131 lines
9.4 KiB
Markdown
131 lines
9.4 KiB
Markdown
---
|
|
extends: arch/db-identifiers.md
|
|
---
|
|
|
|
# Идентификаторы: реализация на Go
|
|
|
|
Как `arch/db-identifiers.md` выглядит в Go-приложении, выбравшем ULID
|
|
(ветка `arch/db-identifiers.md` R1.1). Форма записи — `common/language.md`.
|
|
|
|
Единая точка из `arch/db-identifiers.md` R3 — пакет `internal/ident`: он
|
|
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
|
|
(`Parse`). Правила ниже говорят, из каких мест кода эти функции зовутся.
|
|
|
|
## Правила
|
|
|
|
### R1. Генерация и разбор — только через `internal/ident`
|
|
|
|
**ДОЛЖЕН.** Идентификаторы порождаются и разбираются функциями пакета
|
|
`internal/ident`; других генераторов и парсеров id в коде нет.
|
|
|
|
**Почему.** Реализация `arch/db-identifiers.md` R3 и R4. Вызов
|
|
ULID-библиотеки — одна строка, доступная из любого пакета, и на ревью он не
|
|
выглядит нарушением: значение получается валидное, просто мимо нормализации
|
|
регистра. Отдельный пакет переводит запрет в проверяемое свойство — импорт
|
|
библиотеки где-либо, кроме `internal/ident`, находится поиском по имени
|
|
модуля, а «забытая нормализация» не находится ничем, пока запрос молча не
|
|
перестанет находить существующую запись.
|
|
|
|
### R2. Первичный ключ генерируется в `Create`-методах store
|
|
|
|
**ДОЛЖЕН.** Новая сущность получает значение PK вызовом `ident.NewID()`
|
|
внутри `Create`-метода слоя store.
|
|
|
|
**Почему.** `arch/db-identifiers.md` R2 требует, чтобы значение было
|
|
известно до вставки, но не говорит, кто его присваивает. Store — последний
|
|
слой, через который проходят все пути создания строки, включая импорт,
|
|
фоновые задания и тесты. Генерация выше по стеку делает присвоение
|
|
обязанностью каждого нового вызывающего, и первый забывший запишет пустую
|
|
строку в колонку ключа: для строкового PK это валидное значение, база его
|
|
не отклонит, и дефект обнаружится на второй такой вставке.
|
|
|
|
### R3. Прочие идентификаторы генерируются в точке начала операции
|
|
|
|
**ДОЛЖЕН.** Идентификатор батча, задания или корреляционный ключ создаётся
|
|
вызовом `ident.NewID()` там, где операция начинается.
|
|
|
|
**Почему.** Смысл такого идентификатора (`arch/db-identifiers.md` R7) —
|
|
сшивать записи лога всей операции. Созданный ниже по стеку или в момент
|
|
первой записи в базу, он не покрывает начальные шаги — а именно они нужны,
|
|
когда операция упала до того, как что-либо записала: без общего ключа эти
|
|
записи из лога не собираются вообще.
|
|
|
|
### R4. Бэкфилл в миграциях — `ident.NewIDAt(t)`
|
|
|
|
**ДОЛЖЕН.** Идентификаторы, проставляемые существующим строкам в
|
|
Go-миграции, порождаются с историческим временем строки, а не с текущим.
|
|
|
|
**Почему.** Сортировка id тогда сохраняет историческую хронологию, а не
|
|
момент прогона миграции. Иначе все затронутые строки получают метку одного
|
|
момента, склеиваются в нём и встают в порядке обхода — `ORDER BY id`
|
|
начинает врать ровно на том массиве данных, который старше всего.
|
|
Исправить это потом нельзя: исходное время в идентификаторе не
|
|
восстановить.
|
|
|
|
### R5. Разбор — на входных границах, до обращения к store
|
|
|
|
**ДОЛЖЕН.** `ident.Parse()` вызывается в обработчике HTTP-роута, формы или
|
|
callback'а бота — раньше, чем идентификатор попадёт в store.
|
|
|
|
**Почему.** Реализация `arch/db-identifiers.md` R5. Граница выбрана
|
|
транспортная, потому что только на ней известен источник значения, от
|
|
которого зависит реакция (R8): store видит одинаковую строку независимо от
|
|
того, пришла она из URL или из собственной формы, и ответить по-разному
|
|
оттуда уже невозможно.
|
|
|
|
### R6. Id в структурах — обычный `string`
|
|
|
|
**СЛЕДУЕТ.** Поля идентификаторов в доменных и store-структурах имеют тип
|
|
`string`.
|
|
|
|
**Почему.** Отдельный тип окупается только тогда, когда компилятор ловит им
|
|
ошибку. От перепутывания двух идентификаторов одной семьи (`userID` и
|
|
`authorID`) он не спасает — оба будут одного типа, и различают их имена
|
|
параметров. Зато он требует конверсий на каждой границе с sql-драйвером,
|
|
json и шаблонами, то есть даёт цену без выгоды.
|
|
|
|
### R7. Отдельный тип — когда появляется вторая семья идентификаторов
|
|
|
|
**ДОПУСКАЕТСЯ.** Когда в коде оказываются два вида идентификаторов, которые
|
|
можно перепутать, для них заводятся различимые типы.
|
|
|
|
**Почему.** Явное разрешение нужно, чтобы R6 не читался как запрет на
|
|
типизацию навсегда. Условие названо ровно то, при котором тип начинает
|
|
работать: пока все идентификаторы — `string`, подстановка одного вида
|
|
вместо другого компилируется и обнаруживается только на данных.
|
|
|
|
### R8. Реакция на невалидный id зависит от источника
|
|
|
|
**ДОЛЖЕН.** Когда разбор не удался, ответ определяется тем, откуда пришло
|
|
значение:
|
|
|
|
| № | Источник | Ответ |
|
|
|---|---|---|
|
|
| R8.1 | путь или query URL | 404 без обращения к store |
|
|
| R8.2 | собственная форма, callback-данные кнопки | 400 либо понятное сообщение («кнопка устарела») |
|
|
|
|
**Почему.** Реализация `arch/db-identifiers.md` R5.1 и R5.2 в терминах
|
|
HTTP-кодов. В случае R8.1 снаружи это неотличимо от несуществующей записи —
|
|
и хорошо: чужая или протухшая ссылка описывается так точно. В случае R8.2
|
|
значение сформировало само приложение, и невалидность означает баг
|
|
интерфейса или устаревший экран; ответ «не найдено» здесь выглядит штатно,
|
|
в логах не оставляет аномалии и тем самым съедает единственный момент,
|
|
когда дефект заметен.
|
|
|
|
### R9. Транспорт не создаёт доменные ошибки
|
|
|
|
**НЕ ДОЛЖЕН.** Обработчик не конструирует доменный sentinel (например
|
|
`ErrNotFound`), чтобы тут же сопоставить его со своим ответом.
|
|
|
|
**Почему.** Инверсия правила «трансляция у источника» из
|
|
`lang/go/errors.md`. Sentinel — сообщение от слоя, который знает факт:
|
|
строка не найдена, потому что store её искал. Сфабрикованный транспортом,
|
|
он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли
|
|
вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.
|
|
|
|
<!-- local:механизировано -->
|
|
<!-- /local -->
|
|
|
|
<!-- local:отступления -->
|
|
<!-- /local -->
|