Сервис требует, чтобы каждому клиенту обязательно соответствовал профиль. Почему внешний ключ от профиля к клиенту не гарантирует это правило?
Внешний ключ гарантирует ссылочную целостность только в направлении от дочерней строки к родительской: каждый профиль должен ссылаться на существующего клиента. Он не требует, чтобы для каждой строки клиента существовал профиль. Для обязательной связи со стороны клиента нужны другая модель данных, дополнительное ограничение или триггерная проверка.
Внешние ключи решают исходную проблему появления «висячих» ссылок: дочерняя запись не должна указывать на отсутствующую родительскую. Реляционные ограничения обычно формулируются как запреты недопустимых значений в изменяемой таблице, поэтому внешний ключ естественно контролирует наличие родителя для дочерней строки.
Требование обратного направления имеет другую природу: оно проверяет множество дочерних строк при добавлении или изменении родительской. Поэтому оно не следует автоматически из обычного внешнего ключа.
Пусть таблица профилей содержит обязательный внешний ключ на таблицу клиентов. Тогда нельзя создать профиль для несуществующего клиента, а при удалении клиента можно запретить удаление или настроить каскад. Однако можно создать клиента и не создавать для него профиль — внешний ключ при этом не нарушен.
Если приложение ошибочно считает, что профиль существует всегда, это приводит к неполному чтению данных, ошибкам при соединениях и расхождению между бизнес-правилом и реально гарантируемыми ограничениями базы.
Для связи «один клиент — не более одного профиля» профиль обычно получает внешний ключ, одновременно являющийся его первичным ключом:
Такая схема гарантирует две вещи: профиль не существует без клиента, а у одного клиента не может быть более одного профиля. Но она по-прежнему допускает клиента без профиля, потому что отсутствие дочерней строки не является нарушением внешнего ключа.
Если профиль обязателен для каждого клиента, наиболее простое решение — хранить обязательные атрибуты профиля в самой таблице клиентов. Тогда NOT NULL непосредственно выражает правило. Разделение на две таблицы может быть оправдано большими объёмами необязательных данных, разными правами доступа или отдельным жизненным циклом, но тогда обязательность нужно обеспечивать дополнительно.
Возможные дополнительные механизмы — поддерживаемое конкретной СУБД утверждение уровня схемы, триггер или строго контролируемая транзакционная процедура. Проверка только в приложении слабее: другой сервис, ручной SQL или параллельная операция могут создать клиента без профиля. Триггеры сложнее сопровождать и проектировать с учётом многотабличных операций и конкуренции, поэтому для обязательной тесной связи часто предпочтительнее объединение таблиц.
В системе управления сотрудниками профиль содержит много редко используемых атрибутов и должен быть доступен только отдельной роли. Рассматривались три варианта: оставить две таблицы и проверять создание профиля в приложении, добавить триггер или объединить таблицы.
Проверка в приложении была отклонена из-за нескольких сервисов и риска рассинхронизации. Триггер обеспечивал правило на уровне базы, но усложнял массовую загрузку и тестирование. Выбрали две таблицы с первичным ключом-внешним ключом и триггером, который запрещает фиксацию нового клиента без профиля; операции создания клиента и профиля выполняются в одной транзакции. Это сохранило разделение доступа, а правило стало независимо от конкретного приложения.
NOT NULL на внешнем ключе наличие дочерней строки у каждого родителя?Нет. NOT NULL запрещает пустое значение только в существующей дочерней строке. Он гарантирует, что каждый профиль указывает на клиента, но не заставляет базу создать профиль после добавления клиента.
UNIQUE на внешний ключ профиля?UNIQUE ограничивает связь сверху: один клиент не сможет иметь два профиля. Это обеспечивает кардинальность «не более одного», но не «ровно один». Для обязательности со стороны клиента всё равно требуется объединение сущностей либо отдельный механизм межтабличной проверки.
Одна транзакция защищает только операции, которые действительно выполняются через неё и корректно обрабатывают ошибки. Другой путь записи может создать клиента отдельно, а сбой или ручная операция — оставить его без профиля. Кроме того, конкурентные транзакции требуют согласованной изоляции и блокировок; поэтому транзакционный шаблон полезен, но не равнозначен декларативной гарантии схемы.