конвенции отделены от обвязки
- сами конвенции переехали в conventions/, описательное — в корень: LANGUAGE.md (язык записи) и GUIDE.md (как ведут конвенции) - conv синхронизирует только conventions/, пути в origin даются относительно неё — раскладка копий в репозиториях не меняется
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
---
|
||||
extends: arch/db-identifiers.md
|
||||
---
|
||||
|
||||
# Идентификаторы: реализация на Go
|
||||
|
||||
Как `arch/db-identifiers.md` выглядит в Go-приложении, выбравшем ULID
|
||||
(ветка `arch/db-identifiers.md` R1.1). Форма записи — `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 -->
|
||||
Reference in New Issue
Block a user