Optionality: обязательное и необязательное участие
В первом уроке модуля мы разобрали кардинальность 1:1, 1:N, M:N — она отвечает на вопрос «сколько строк одной таблицы соответствует строке другой». Но у связи есть и второй параметр, не менее важный: «а обязана ли строка вообще участвовать в связи?». Это optionality (опциональность), и формально это минимальная кардинальность связи.
Кардинальность 1:N говорит про максимум: «у пользователя может быть много заказов» — но может ли быть ноль? Обязан ли каждый пользователь иметь хотя бы один заказ? Это и есть вопрос optionality. Junior-инженер, который проектирует связь, но не задаёт этот вопрос, получит схему, которая разрешает бессмысленные состояния данных — или, наоборот, запрещает нормальные.
Минимальная и максимальная кардинальность
У каждого конца связи есть две границы:
- Максимальная кардинальность — «сколько максимум»: один или много. Это то, что мы обозначаем как 1 или N в записи 1:N.
- Минимальная кардинальность — «сколько минимум»: ноль или один. Это и есть optionality.
Минимальная кардинальность принимает ровно два значения:
- Mandatory (обязательное участие), минимум 1 — строка обязана участвовать в связи. Не может быть строки без связанной.
- Optional (необязательное участие), минимум 0 — строка может участвовать, а может и нет. Допустима строка без связанной.
Полная характеристика конца связи — это пара «минимум..максимум». Стандартные обозначения:
0..1 optional, максимум один (ноль или один)
1..1 mandatory, максимум один (ровно один)
0..N optional, максимум много (ноль или больше)
1..N mandatory, максимум много (один или больше)
В нотации Crow’s Foot (вы видели её в модуле про ER-моделирование) optionality показывается символом у конца линии: кружок = optional (ноль допустим), чёрточка = mandatory (минимум один).
Optionality на практике: разбираем связь по сторонам
Optionality задаётся для каждого конца связи отдельно — две стороны могут быть разными. Разберём связь users 1:N orders с двух сторон.
Сторона orders (заказ -> пользователь). Может ли существовать заказ без пользователя? Нет — заказ всегда кем-то сделан, «ничей» заказ бессмыслен. Значит, со стороны заказа участие mandatory: у каждого заказа обязан быть пользователь.
Сторона users (пользователь -> заказы). Может ли существовать пользователь без единого заказа? Да — человек зарегистрировался, но ещё ничего не купил. Это нормальное, валидное состояние. Значит, со стороны пользователя участие optional: у пользователя может быть ноль заказов.
Итог: одна связь, но разная optionality на концах. Со стороны orders — обязательно (1..1: ровно один пользователь). Со стороны users — необязательно (0..N: ноль или больше заказов).
users ----0..N---- orders ----1..1---- (обратно к users)
users -> orders: optional (пользователь может иметь 0 заказов)
orders -> users: mandatory (заказ обязан иметь 1 пользователя)
Как optionality реализуется в SQL
Вот где optionality становится конкретным кодом. Минимальная кардинальность со стороны «многих» (то есть на дочерней таблице, где лежит foreign key) реализуется через NULL-допустимость столбца foreign key.
Mandatory участие -> foreign key объявлен NOT NULL. Если заказ обязан иметь пользователя, то столбец orders.user_id не может быть пустым. NOT NULL это и гарантирует.
Optional участие -> foreign key допускает NULL. Если связь со стороны дочерней таблицы необязательна, столбец FK может быть NULL — NULL означает «связи нет».
-- Заказ ОБЯЗАН иметь пользователя: mandatory -> NOT NULL
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id), -- mandatory
amount NUMERIC(12,2) NOT NULL
);
-- Вставить заказ без user_id невозможно — NOT NULL не позволит.
-- Сотрудник МОЖЕТ не иметь руководителя (директор): optional -> NULL разрешён
CREATE TABLE employees (
employee_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
manager_id INTEGER REFERENCES employees(employee_id) -- optional, NULL OK
);
-- У директора manager_id = NULL — связи с руководителем нет, это нормально.
Запомните соответствие — оно прямое и его легко применять:
| Optionality (минимальная кардинальность) | Реализация для FK на дочерней таблице |
|---|---|
| Mandatory (минимум 1) | FOREIGN KEY объявлен NOT NULL |
| Optional (минимум 0) | FOREIGN KEY допускает NULL |
Со стороны «одного» (родительской таблицы) optionality в обычной схеме напрямую через NULL не выражается — там нет столбца FK. «У пользователя минимум один заказ» (mandatory со стороны users) обычным NOT NULL не обеспечить: пользователь и его первый заказ создаются разными INSERT, и между ними пользователь временно без заказов. Такое правило либо реализуют сложнее (отложенная проверка, триггер), либо признают, что в реляционной схеме оно соблюдается на уровне приложения. Поэтому на практике mandatory-минимум чаще всего реально работает именно со стороны дочерней таблицы — через NOT NULL на FK.
Почему optionality — важное проектное решение
Может показаться, что optionality — мелочь: «ну NULL или NOT NULL, какая разница». Разница большая, и вот почему.
Optionality защищает от бессмысленных данных. Если вы забыли поставить NOT NULL на orders.user_id, в таблицу попадут заказы с user_id = NULL — «ничьи» заказы. Любой отчёт «выручка по пользователям» их потеряет (вспомните урок про NULL: JOIN по NULL не находит пару, WHERE отбрасывает). Это тихая потеря данных, которую заметят не сразу.
Optionality влияет на тип JOIN. Если связь со стороны дочерней таблицы mandatory (FK всегда заполнен), то INNER JOIN и LEFT JOIN дадут одинаковый результат — пара найдётся всегда. Если optional (FK бывает NULL), разница принципиальна: INNER JOIN потеряет строки без связи, LEFT JOIN сохранит их с NULL. Понимание optionality подсказывает, какой JOIN писать.
Optionality — это знание о предметной области. «Заказ обязан иметь клиента, сотрудник может не иметь руководителя» — это факты о том, как устроен бизнес. Зафиксировать их в схеме (через NOT NULL или его отсутствие) — то же самое, что зафиксировать constraint: правило переходит из головы разработчика в гарантию СУБД.
Процедура: определить optionality связи
Дополним алгоритм из первого урока. Для связи между A и B, помимо вопросов «сколько максимум» (кардинальность), задайте вопросы про минимум:
- Может ли A существовать без связанного B? Да -> со стороны A связь optional. Нет -> mandatory.
- Может ли B существовать без связанного A? Да -> со стороны B optional. Нет -> mandatory.
- Для конца, где лежит foreign key (дочерняя таблица): mandatory -> объявите FK как
NOT NULL; optional -> оставьте FK nullable.
Пример. Связь employees и department (отдел). Может ли сотрудник существовать без отдела? Допустим, по правилам компании — нет, каждый сотрудник приписан к отделу. Значит, со стороны employees участие mandatory -> employees.department_id объявляем NOT NULL. Может ли отдел существовать без сотрудников? Да — новый отдел только создали, людей ещё не набрали. Со стороны department — optional (но это сторона «одного», и через NULL не выражается — см. callout выше).
Попробуй сам
Для каждой связи определите optionality с обеих сторон по процедуре и реализуйте в SQL:
ЗаказиСпособ оплаты. Может ли заказ существовать без указанного способа оплаты (например, заказ оформлен, но ещё не оплачен)? Решите, mandatory или optional участие со стороны заказа, и реализуйте FK соответственно —NOT NULLили nullable.СотрудникиРабочий ноутбук(1:1). Может ли сотрудник быть без ноутбука? Может ли ноутбук лежать на складе, не закреплённый ни за кем? Опишите optionality обоих концов.СтатьяиАвтор. Может ли статья существовать без автора? Реализуйте.- Возьмите таблицу
ordersиз примеров урока. Сознательно создайте её БЕЗNOT NULLнаuser_id, вставьте один заказ сuser_id = NULL, затем напишитеINNER JOINсusersиLEFT JOINсusers. Сравните результаты и объясните, какую строку и почему теряетINNER JOIN. Это наглядно показывает, зачем нужна правильная optionality.