Юрий Белк – Full stack Developer (страница 34)
– Audit log
– Idempotency keys (для безопасного повтора POST)
Сразу полезное правило: таблица – это не “модель в коде”, а “факт в мире”.
Например, “участник workspace” – это факт, значит это отдельная таблица membership.
9.3. Нормализация: что это вообще и зачем вам это сегодня
Нормализация – это не ритуал. Это способ уменьшить вероятность того, что:
– вы храните одно и то же в пяти местах,
– оно расходится,
– и потом вы неделями чините отчёты и поиск.
9.3.1. Практическое правило №1: “Один факт – одно место”
Если роль пользователя в workspace хранится и в users, и в workspace_members, и в каком-то JSON – однажды вы поймаете ситуацию: “в одном месте admin, в другом member”. И вы проиграете спор с реальностью.
Роль в рамках workspace – это факт membership. Значит:
– workspace_members.role – единственный источник.
9.3.2. Практическое правило №2: “Массивы и списки – осторожно”
Например, хочется хранить метки как массив строк прямо в tasks.labels.
Это удобно первые два дня. Потом вы захотите:
– переименовать метку,
– вывести список всех меток workspace,
– посчитать статистику по меткам,
– не допускать дубликаты,
– индексировать фильтр по метке.
И внезапно вы понимаете, что метка – сущность, и у неё должна быть таблица.
Вывод: метки – отдельная таблица labels, а связь – task_labels.
9.3.3. Практическое правило №3: “Денормализация допустима, но осознанно”
Мы можем хранить workspace_id в tasks, даже если его можно вычислить через project. Это “снятие лишнего JOIN” для производительности и удобства фильтров.
Но тогда нужен механизм, который гарантирует консистентность (например, constraint, триггер, или правило в коде + тесты). В учебной системе можно держать workspace_id в tasks и контролировать его в приложении, а в идеале – усилить constraint’ами.
9.4. Схема данных: таблицы и ключевые поля
Ниже – списками, без таблиц (как вы просили). Названия – в стиле snake_case.
9.4.1. users
– id (uuid или bigserial; для современных систем uuid очень удобен)
– email (unique, not null)
– password_hash (not null)
– name (nullable)
– created_at (not null, default now())
– updated_at (not null, default now())
– deleted_at (nullable) – если хотите soft delete для пользователей (часто не надо; чаще “deactivated”)
Индексы:
– уникальный индекс на email
Constraints:
– email не пустой (можно простое CHECK (email <> ''))
– лучше хранить email в lower-case или нормализовать на входе
9.4.2. workspaces
– id
– name (not null)
– owner_user_id (not null, FK → users.id)
– created_at, updated_at
– deleted_at (nullable, если soft delete workspace)
Индексы:
– индекс на owner_user_id
Constraints:
– FK на owner
– CHECK (name <> '')
9.4.3. workspace_members
Это центральная таблица для прав.
– workspace_id (FK → workspaces.id)
– user_id (FK → users.id)
– role (not null; например: owner/admin/member)
– created_at
Ключи:
– составной PK (workspace_id, user_id) – так проще запретить дубль участия.
Constraints:
– role ограничить через CHECK (role IN ('owner','admin','member'))
или через PostgreSQL ENUM (ENUM удобен, но миграции могут быть чуть менее приятными; CHECK проще менять).
Индексы:
– индекс на user_id (быстрый поиск “где состоит пользователь”)