Identifying vs non-identifying relationships
Мы научились различать связи по кардинальности (1:1, 1:N, M:N) и по optionality (mandatory/optional). Есть ещё одно различие связей, важное для проектирования: identifying против non-identifying. Оно отвечает на вопрос: входит ли foreign key родителя в primary key ребёнка, или лежит в дочерней таблице как обычный столбец.
Это различие напрямую связано с понятиями weak и strong entity из модуля про ER-моделирование, и оно определяет, может ли дочерняя строка вообще существовать самостоятельно. Junior-инженеру это нужно, чтобы правильно строить ключи дочерних таблиц — особенно junction-таблиц.
Два вида связи: где живёт foreign key в ребёнке
В связи через foreign key есть parent (родитель) и child (ребёнок). Foreign key всегда лежит в дочерней таблице. Вопрос — какую роль он там играет.
Non-identifying relationship (неидентифицирующая связь). Foreign key родителя лежит в дочерней таблице как обычный атрибут, НЕ входящий в её primary key. У ребёнка есть свой собственный, независимый primary key.
Identifying relationship (идентифицирующая связь). Foreign key родителя входит в состав primary key дочерней таблицы. Ребёнок не имеет полностью собственного ключа — его идентичность частично или полностью определяется родителем.
Разберём на двух примерах.
Пример non-identifying: users и orders
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY, -- свой независимый PK
user_id INTEGER NOT NULL REFERENCES users(user_id), -- FK — обычный столбец
amount NUMERIC(12,2) NOT NULL
);
Здесь связь users -> orders — non-identifying. Смотрим на orders: её primary key — order_id, и он полностью свой, родителя в нём нет. Foreign key user_id лежит в orders отдельным столбцом, в primary key он не входит. Заказ идентифицируется собственным order_id независимо от пользователя.
Пример identifying: orders и order_items
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id),
amount NUMERIC(12,2) NOT NULL
);
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(order_id), -- FK...
line_no INTEGER NOT NULL,
product_id INTEGER NOT NULL REFERENCES products(product_id),
quantity INTEGER NOT NULL,
PRIMARY KEY (order_id, line_no) -- ...и FK ВХОДИТ в primary key!
);
Здесь связь orders -> order_items — identifying. Смотрим на order_items: её primary key — составной (order_id, line_no), и order_id — это foreign key на orders, который входит в primary key. Позиция заказа не имеет полностью собственного ключа: она идентифицируется как «позиция номер line_no внутри заказа order_id». line_no сам по себе («позиция 1») не значит ничего — нужен ещё order_id.
Связь с weak и strong entity
Это различие — прямое продолжение темы weak/strong entity из модуля про ER-моделирование. Напомним кратко:
- Strong (regular) entity — сущность с собственным ключом, существует самостоятельно.
- Weak entity — сущность без собственного полного ключа; идентифицируется только через родительскую сущность плюс свой частичный ключ (discriminator).
Соответствие прямое:
- Non-identifying relationship связывает родителя со strong entity. Ребёнок самодостаточен: у заказа есть
order_id, он существует и опознаётся сам по себе. - Identifying relationship связывает родителя со weak entity. Ребёнок несамодостаточен: позиция заказа
order_items— это weak entity, она не существует без своего заказа и опознаётся только в паре «заказ + номер позиции».
strong entity <-- non-identifying -- parent (ребёнок самодостаточен)
weak entity <-- identifying -- parent (ребёнок зависит от родителя)
В нотации Crow’s Foot identifying-связь рисуют сплошной линией (и дочернюю сущность — со скруглёнными углами как признак weak entity), non-identifying — пунктирной. Но главное — не картинка, а суть: входит FK в PK ребёнка или нет.
Практические следствия
Различие identifying/non-identifying — не теоретическое украшение. Оно влияет на три вещи.
1. Существование дочерней строки. В identifying-связи дочерняя строка не может существовать без родителя — это вшито в саму структуру ключа. Раз order_id входит в primary key order_items, а primary key обязан быть NOT NULL (урок про ключи), то order_id в order_items физически не может быть пустым. Позиция заказа без заказа невозможна по построению. В non-identifying связи, если FK nullable, ребёнок может существовать без родителя (это была тема прошлого урока про optionality).
2. Каскадное удаление логично по умолчанию. Для identifying-связи ON DELETE CASCADE — естественный выбор: если weak entity не существует без родителя, то при удалении родителя дочерние строки тоже должны исчезнуть (удалили заказ — его позиции бессмысленны). Для non-identifying связи каскад — не само собой разумеющийся выбор: удаление пользователя не обязано удалять заказы, тут возможен и RESTRICT, и SET NULL.
3. Распространение ключа вглубь. Identifying-связи складываются. Если C идентифицируется через B, а B — через A, то primary key C включает ключ B, который включает ключ A. Цепочка identifying-связей наращивает составной ключ всё шире на каждом уровне. Это работает, но широкие составные ключи неудобны (вспомните, почему — модуль про ключи: ширина, JOIN, foreign keys). Поэтому глубокие цепочки identifying-связей часто разрывают, вводя на каком-то уровне surrogate key и превращая связь в non-identifying.
Junction table — почти всегда identifying-связь с ОБЕИХ сторон. Вспомните enrollments с primary key (student_id, course_id): оба столбца — foreign key, и оба входят в primary key. Значит, enrollments идентифицируется через обоих родителей — связь identifying и со стороны students, и со стороны courses. Это естественно: строка-связь не существует без обеих сторон, которые она соединяет. Поэтому простая junction table — классический пример weak entity.
Как выбрать: identifying или non-identifying
Junior-инженеру нужен критерий. Спросите про дочернюю сущность:
Может ли дочерняя строка быть осмысленно идентифицирована САМА ПО СЕБЕ, без родителя?
- Да — у неё есть естественный собственный ключ (заказ ->
order_id, пользователь ->user_id). Это strong entity, связь делайте non-identifying: дайте ребёнку свой primary key, а FK родителя положите обычным столбцом. - Нет — её идентичность имеет смысл только внутри родителя (позиция -> только «позиция N в заказе X»; строка-связь -> только «связь A и B»). Это weak entity, связь делайте identifying: включите FK родителя в составной primary key ребёнка.
Пример. Сущность «комментарий к статье». Может ли комментарий быть идентифицирован сам по себе? Да — у комментария вполне может быть свой comment_id, он самодостаточен как объект. Значит, strong entity, связь articles -> comments — non-identifying: comments получает свой comment_id PK, а article_id — обычный FK-столбец.
Контрпример. Сущность «строка спецификации товара» — характеристика товара по порядковому номеру. Имеет ли «характеристика номер 3» смысл без товара? Нет — нужен товар. Weak entity, связь identifying: primary key (product_id, spec_no).
Тонкость: identifying-связь и surrogate key
В уроках про ключи мы сделали surrogate key стандартом для primary key. Как это сочетается с identifying-связями, где ключ ребёнка должен включать ключ родителя?
Тут есть нюанс, который стоит понимать чётко. Identifying-связь — это про логику модели: дочерняя сущность семантически зависит от родителя, она weak entity. Это утверждение о предметной области, и оно остаётся верным независимо от того, какие ключи вы выберете физически.
На физическом уровне есть два способа реализовать эту зависимость:
- Естественный путь. Сделать primary key ребёнка составным, включив в него foreign key родителя:
order_items (order_id, line_no). Тогда зависимость weak entity вшита прямо в ключ — это «чистая» identifying-связь. - Surrogate-путь. Дать ребёнку собственный surrogate primary key
order_item_id, а зависимость от родителя выразить черезNOT NULLforeign key плюс, при необходимости,UNIQUE (order_id, line_no). Связь формально становится non-identifying (FK не в primary key), но семантика weak entity сохраняется: позиция всё равно не существует без заказа.
Оба способа корректны, и второй на практике встречается часто — именно потому, что узкий surrogate key удобнее для дальнейших ссылок (модуль про ключи объяснил, почему). Важно не путать уровни: «weak entity» — это про смысл, «identifying relationship» — про конкретную технику с составным ключом. Можно иметь weak entity, реализованную через non-identifying связь и surrogate key.
Практический ориентир: глубину цепочки identifying-связей стоит держать небольшой. Если C идентифицируется через B, B через A, B-через-A-через-предка — составной primary key на нижнем уровне разрастается, и каждая ссылающаяся таблица тащит всё более широкий foreign key. На втором-третьем уровне вложенности обычно разумно перейти на surrogate key: это разрывает накопление ширины ключа, не теряя смысла зависимости.
Попробуй сам
Для каждой пары «родитель -> ребёнок» определите, identifying связь или non-identifying, по критерию «самодостаточен ли ребёнок», и напишите CREATE TABLE дочерней таблицы с правильным primary key:
Дом->Квартира. Может ли квартира быть идентифицирована сама по себе, или только как «квартира номер N в доме X»? Какой primary key уapartments?Клиент->Договор. Имеет ли договор собственный осмысленный номер? Identifying или non-identifying?Книга->Глава. Глава опознаётся как «глава 5» вообще или как «глава 5 книги X»? Постройте primary key таблицыchapters.Университет->Студент. Студент — самодостаточная сущность или существует только внутри университета? Решите и обоснуйте.- Возьмите identifying-пример из задания (
apartmentsилиchapters) и постройте на нём цепочку из трёх уровней: напримерДом -> Подъезд -> Квартира, где каждая связь identifying. Посмотрите, как растёт составной primary key на третьем уровне, и подумайте, в какой момент имеет смысл разорвать цепочку через surrogate key.