Команда моделирует систему, где один пользователь может иметь несколько ролей, а каждая роль назначается многим пользователям. Как представить такую связь в реляционной модели?
Связь «многие ко многим» следует представить через отдельную ассоциативную сущность: таблицу назначений, которая содержит ссылки на пользователя и роль. Обычно пара этих ссылок образует уникальную комбинацию, чтобы одно и то же назначение нельзя было создать повторно.
Реляционная модель изначально стремилась представлять данные в виде отношений с атомарными значениями и явными связями между сущностями. Хранение списка ролей внутри записи пользователя нарушает эту идею и затрудняет поиск, проверку ссылочной целостности и изменение отдельных назначений.
Ассоциативная сущность решает эту проблему универсальным способом: каждая строка описывает один факт назначения конкретной роли конкретному пользователю.
Если хранить роли в одном поле пользователя, например в списке или строке с разделителями, база данных не сможет нормально обеспечить ссылку на существующую роль. Поиск пользователей с определённой ролью, удаление роли и контроль дубликатов потребуют специальной логики приложения.
Прямая ссылка только из пользователя на роль тоже недостаточна: она позволяет сохранить лишь одну роль. Набор отдельных полей вроде role_1, role_2 создаёт искусственное ограничение и приводит к повторению структуры.
Создаётся отдельная сущность назначения с двумя внешними ключами: на пользователя и на роль. Логически она представляет отношение между сущностями, а не самостоятельный бизнес-объект без контекста.
Минимальная структура может выглядеть так:
Составной первичный ключ запрещает повторное назначение одной роли одному пользователю. Внешние ключи обеспечивают ссылочную целостность: нельзя назначить несуществующую роль или несуществующего пользователя.
Если само назначение имеет свойства, они добавляются в ассоциативную сущность: дату начала действия, дату окончания, источник назначения или признак автоматического предоставления. Тогда это уже не просто техническая таблица связи, а полноценная предметная сущность с собственными правилами.
Компромисс состоит в том, что чтение ролей требует соединения таблиц, а операции назначения становятся отдельными вставками и удалениями. Зато модель остаётся расширяемой, проверяемой базой данных и пригодной для сложных запросов.
Важно определить правила удаления. Например, удаление пользователя может удалять его назначения каскадно, тогда как удаление роли может быть запрещено, если она используется. Выбор зависит от требований к истории и аудиту.
В корпоративном портале сотрудник мог одновременно быть оператором и руководителем, а одна роль назначалась сотням сотрудников. Рассматривались три варианта: хранить роли в JSON-поле пользователя, завести фиксированный набор колонок ролей или создать таблицу назначений.
JSON был удобен для первоначальной разработки, но усложнял проверку существования ролей и выборки по ним. Фиксированные колонки были просты, однако не позволяли добавлять роли без изменения схемы и ограничивали их количество.
Выбрали таблицу назначений с внешними ключами и уникальностью пары «сотрудник–роль». Для временных полномочий в неё добавили даты действия. В результате новые роли стали добавляться без изменения модели пользователя, а база данных смогла сама предотвращать несуществующие и повторные назначения.
Без ограничения одна и та же роль может быть назначена пользователю несколько раз. Это приведёт к дублированию результатов запросов, ошибкам подсчёта полномочий и неоднозначности при отзыве роли. Если повторные назначения действительно являются отдельными событиями, их нужно моделировать другой сущностью — например, журналом назначений, а не текущим состоянием связи.
Составного ключа достаточно, если назначение однозначно определяется парой ссылок и не имеет самостоятельных ссылок извне. Отдельный идентификатор полезен, когда на назначение ссылаются другие сущности, у него сложный жизненный цикл или требуется различать несколько исторических записей для одной пары.
При этом отдельный идентификатор не отменяет бизнес-ограничения. Если одновременно допускается только одно активное назначение роли пользователю, это правило нужно выразить отдельным уникальным ограничением или другой поддерживаемой базой данных конструкцией.
Текущая связь отвечает на вопрос, действует ли назначение сейчас. История отвечает на вопросы о том, кто, когда и почему назначал или отзывал роль. Если хранить только одну строку и физически удалять её при отзыве, восстановить прошлое состояние нельзя.
Для аудита обычно выделяют события или исторические записи с периодами действия, автором изменения и причиной. Нельзя автоматически считать таблицу текущих назначений полноценной историей: она фиксирует состояние, но не обязательно сохраняет последовательность переходов.