PHP
System Analysis and Design
SQL
July 2015 5

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

From Sandbox
Недавно, делая очередной функционал на одном из проектов, я столкнулся с немного необычными связями в реляционных СУБД, у которых, как оказалась позже, есть замысловатое название — Полиморфные связи. Что это такое, как и где их применять, я попытаюсь объяснить в данной статье.

Тема полиморфных связей уже поднималась не раз на Хабре («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 с дополнительными условиями при прописывании связей.

Из всего вышесказанного, новичкам стоит обратить внимание на то, когда использовать полиморфные связи. Не стоит их пихать направо и налево, из проекта в проект, только потому что это круто. Нужно немного прикинуть наперед, а появятся ли завтра новые таблицы, новые сущности с одинаковым функционалом и требованиями, которые можно было бы вынести и сделать динамическими, и исходя от ответа проектировать свои БД.
+10
42.3k 187
Comments 39
Top of the day