Поліморфні зв'язку для самих маленьких

Нещодавно, роблячи черговий функціонал на одному з проектів, я зіткнувся з трохи незвичними зв'язками в реляційних СУБД, у яких, як виявилося пізніше, є хитромудре назва — Поліморфні зв'язку. Що це таке, як і де їх застосовувати, я спробую пояснити в даній статті.

Тема поліморфних зв'язків вже піднімалася не раз на Хабре («Rails і поліморфні зв'язку», «Поліморфні наскрізні асоціації в Ruby on Rails», «Поліморфні зв'язку»), але вона піднімалася в контексті Ruby, і для тих, хто вже має якийсь досвід в проектуванні БД. Новачкам же (мені було), мало що зрозуміло з тих статей, тому в даній статті я спробую розповісти все на пальцях, абстрагуючись від мови, хіба що трохи зачеплю ORM популярних фреймворків у вебі.

Всім зрозумілі звичайні «взаємини» табличок в реляційних БД: один-до-одного, один-до-багатьох, багато-до-багатьох. А якщо не зрозумілі, то ось вам прості приклади їх реалізації.

Один-к-одному. Одного запису з першої таблиці відповідає тільки один запис із другої таблиці. Тут все просто. Найпоширеніший приклад — таблиця user і user_profile (Кожному користувачеві, відповідає один профіль).

Один-ко-багатьом. Зв'язок будується таким чином, що кожний запис в одній таблиці може відповідати кілька записів в іншій таблиці. Приклад — таблиця articles (статті), таблиця comments (коментарі). До однієї статті може бути залишено безліч коментарів.

Багато-до-багатьом. Зв'язок реалізується, коли одному рядку з однієї таблиці може відповідати кілька записів з іншої і навпаки. Хороший приклад — є таблиця статей (articles), є таблиця тегів (tags), зв'язуються вони через проміжну таблицю (pivot table або junction table) tags_articles, в якій є article_id, tag_id.
Начебто, все просто і зрозуміло.

Звідки ж взялися якісь поліморфні зв'язку, якщо і так попередні зв'язку цілком логічні і ніби, не вимагають доповнень?
Попередні зв'язки (один-до-одного, один-до-багатьох, багато-до-багатьох), створюються для статичних сутностей із таблиць, на яких можна навісити обмежень (constraints) надаються СУБД.

Повернемося до прикладу зв'язку один-до-багатьох.

+--------------+
| articles |
| comments |
+--------------+

articles:
+----+--------------------------------------------------------+------------+
| id | text | date |
+----+--------------------------------------------------------+------------+
| 1 | крутий Текст статті | 2015-07-05 |
| 2 | Текст ще однією крутий статті | 2015-07-05 |
+----+--------------------------------------------------------+------------+

comments:
+----+----------------------------------------------------------------+------------+------------+
| id | text | article_id | created_at |
+----+-----------------------------------------------------------------------------+------------+
| 1 | Непоганий комент| 1 | 2015-07-05 |
| 2 | Непоганий комент| 1 | 2015-07-05 |
| 3 | Непоганий комент| 2 | 2015-07-05 |
+----+----------------------------------------------------------------+------------+------------+

В таблиці comments article_id — це id статті з таблиці articles. Все очевидно. Але! Що якщо, завтра у нас з'являється необхідність створити таблицю news (новин) і для неї теж потрібно додати функціонал коментарів?!

При відомих нам типів зв'язків між таблицями, з'являється два варіанти:
1) Створити нову таблицю comments (напр. comments_news) з ідентичною структурою, як у таблиці comments, але замість article_id, поставити news_id.
2) В існуючу таблицю comments додати ще один стовпець news_id поруч з article_id.

В обох випадках виходить якось кострубато. Якщо завтра потрібно буде додати функціонал коментарів до ще однієї — третьої таблиці (напр. до постів користувачів або до картинок), нам доведеться створити ще одну таблицю або третє поле існуючої таблиці? П'яте-десяте? Не то… Тут на допомогу і приходять поліморфні зв'язку.

Суть поліморфних зв'язків
Поліморфні зв'язку — це динамічні зв'язки між таблицями з використанням типу сутності.
Щоб було зрозуміло, трохи змінимо наші таблиці і зробимо між ними поліморфні зв'язку.

Наша ще одна таблиця — news:
+----+--------------------------------+------------+
| id | text | date |
+----+--------------------------------+------------+
| 1 | Якась новина | 2015-07-05 |
+----+--------------------------------+------------+

І міняємо таблицю comments, щоб стало, рівно!

comments:
+----+----------------------------------------------------+-----------+-------------+------------+
| id | text | entity_id | entity_type | created_at |
+----+----------------------------------------------------+-----------+-------------+------------+
| 1 | Непоганий коммент | 1 | article | 2015-07-05 |
| 2 | Непоганий коммент | 1 | article | 2015-07-05 |
| 3 | Непоганий коммент | 2 | article | 2015-07-05 |
| 4 | Коммент | 1 | news | 2015-07-05 |
+----+----------------------------------------------------+-----------+-------------+------------+

Суть поліморфних зв'язків стає ясна, при перегляді таблиці comments — entity_id — id якоїсь сутності, до якої ми залишаємо коментар, entity_type — тип цієї самої сутності. Ні entity_id, ні entity_type — заздалегідь невідомі, тому ці зв'язки можна назвати динамічними.

Використовувати поліморфні зв'язку варто тоді, коли у нас з'являється два і більше таблиці, у яких буде зв'язок один-ко-багатьом з якоюсь іншою однієї і тієї ж таблиці (articles-comments, news-comments, posts-comments тощо). Якщо ж у вас є зв'язки лише між 2 таблицями і більше не передбачається, поліморфні краще замінити на звичайні один-до-багатьох.

Поліморфні зв'язку можуть бути реалізовані, і як багато-до-багатьох.
Показувати таблиці з даними не має сенсу, покажу лише приблизну структуру.
articles:
id — integer
text — text

posts:
id — integer
text — text

tags:
id — integer
name — string

tags_entities
tag_id — integer
tag_entity_id — integer
tag_entity_type — string (post|article)

Мінуси поліморфних зв'язків
Не все так ідеально, як могло б здатися на перший погляд. В силу своєї динамічної природи поліморфних зв'язків між полями связуемых таблиць, можна проставити зв'язку зовнішніх ключів (foreign key) використовуючи СУБД, а тим більше і обмежень (constraints) на зміну або видалення записів. Це, власне найбільший мінус поліморфних зв'язків. Доведеться, або писати свої тригери (процедури або ще щось) для самої СУБД, або, що частіше роблять, перекласти роботу по синхронізації рядків і накладання обмежень між таблицями на ORM і мова програмування.

Другий, вже менш значний мінус поліморфних зв'язків полягає в типі сутності. Необхідно якось описати який тип, який таблиці належить. Це може бути не очевидно, якщо наприклад назву таблиці змінилося або якщо ви задали тип сутності цифрами. Вирішити цю проблему можна, наприклад створивши окрему таблицю, або прописавши в коді проекту, асоціативний масив з зіставленням типу та сутності.

Робота ORM з поліморфними зв'язками
Слід сказати, що сучасні фреймворки та їх ORM без особливих складнощів, здатні працювати з даними зв'язками.
Наприклад, як вже говорилося вище, Ruby on Rails підтримує їх з коробки. Php-фреймворк Laravel, також має у своїй реалізації ORM для таких типів зв'язків зручні методи (morphTo, morphMany і т. д.), а як тип сутності використовує повна назва класу моделі. У фреймворку Yii2, ні з коробки якихось специфічних методів для такого роду зв'язків, але вони можуть бути реалізовані через звичайні методи hasOne, hasMany з додатковими умовами при прописуванні зв'язків.

З усього вищесказаного, новачкам варто звернути увагу на те, коли використовувати поліморфні зв'язку. Не варто пхати їх направо і наліво, із проекту у проект, тільки тому що це круто. Потрібно трохи наперед прикинути, а з'являться завтра нові таблиці, нові сутності з однаковим функціоналом і вимог, які можна було б винести і зробити динамічними, і виходячи від відповіді проектувати свої БД.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.