Наследование Entity в Doctrine

4edb64aebfbc0420d3c35d09c3ee1ee9.jpg

В практике разработки веб-приложений иногда возникает необходимость расширения сущностей, которые представляют таблицы базы данных в коде. Для примера рассмотрим следующую ситуацию: в нашем проекте была реализация класса автотранспортного средства Car, но спустя некоторое время появилась возможность ввести еще один класс автотранспортного средства под названием Buggy. Новый класс, имел одинаковые поля и представлял схожую концепцию. Нам важно было иметь возможность работать с ним как с объединенным типом Auto, а также как с отдельным типом.

Разделение сущностей на два отдельных объекта привело бы к значительному рефакторингу кода и переписыванию множества методов для работы с новым классом. Кроме того, в будущем возможно появление новых родственных сущностей, и мы не хотели создавать фундамент для их бесконечного клонирования. После изучения возможных решений, мы остановились на готовом механизме наследования сущностей.

Варианты решения

ORM Doctrine предлагает три варианта наследования сущностей, и давайте рассмотрим каждый из них по порядку, а также выясним их преимущества и недостатки.

MappedSuperclass

Сопоставленный суперкласс — это абстрактный или конкретный класс, который не является сущностью, но обеспечивает постоянное состояние сущности и хранение информации о сопоставлении для своих подклассов. Основная цель такого сопоставленного суперкласса заключается в определении общей информации о состоянии и сопоставлении, которая применима для нескольких классов сущностей.

Сопоставленные суперклассы, подобно обычным несопоставленным классам, могут располагаться в середине иерархии наследования, которая обычно отображается с использованием одной таблицы или таблицы наследования классов.

MappedSuperclass не может быть самостоятельной сущностью, и он не поддерживает запросы. Постоянные связи, определенные в суперклассе, должны быть однонаправленными. Это означает, что ассоциации «один-ко-многим» вообще невозможны. Кроме того, обращения «многие-ко-многим» возможны только в том случае, если сопоставленный суперкласс используется только в одном объекте одновременно. Поддержка осуществляется с помощью функции наследования одиночной или объединенной таблицы.

Пример наследования в коде:

В миграции мы получили таблицу, в которой отображения определены непосредственно в суперклассе и наследуются подклассами.

Плюсы

  • Нет необходимости вносить изменения на уровне базы данных.

  • Простой вариант объединения части полей родственных сущностей в один класс, сохраняющий чистоту кода и централизованное управление общими полями, что особенно актуально, если родственных сущностей много.

Минусы

  • Отсутствие возможности выполнять запросы к родительскому классу.

  • Необходимость написания сложных запросов для объединения двух таблиц.

  • Конфликт при запросах из-за повторов первичных ключей.

  • Не дает особых преимуществ, кроме чистоты кода.

JoinedTable

Стратегия наследования таблиц классов предполагает сопоставление каждого класса в иерархии с несколькими таблицами: собственной таблицей и таблицами всех родительских классов. При этом таблица дочернего класса связывается с таблицей родительского класса с помощью внешнего ключа. В Doctrine ORM эта стратегия реализуется с использованием дискриминатора, который находится в верхней таблице иерархии. Дискриминатор представляет собой простой способ осуществления полиморфных запросов с учетом наследования таблиц классов. Родительский класс будет выглядеть следующим образом:

Пример дочернего класса:

В результате создаются две таблицы: по одной для каждой сущности в иерархии классов. Каждая таблица содержит только поля, объявленные в соответствующем классе сущности. Важно отметить, что создается внешний ключ child_entity.id → parent_entity.id.

Плюсы

  • Создание таблиц для каждого класса позволяет экономить место на диске, так как избегаются нулевые поля, которые возникают при наследовании.

  • Присутствие внешнего ключа с родительской таблицей помогает избежать конфликтов с первичными ключами.

Минусы

  • Запросы через Doctrine обрабатываются дольше из-за объединения нескольких таблиц.

  • При внедрении в проект потребуется переписывать старые методы и запросы, чтобы учесть новую структуру таблиц.

SingleTable

Это подход, при котором поля нескольких классов размещаются в одной таблице в базе данных, что позволяет сократить количество операций объединения JOIN при выборке из СУБД. Для реализации этого подхода необходимо создать родительский класс и применить следующие аннотации:

  • @InheritanceType: указывает тип наследования.

  • @DiscriminatorColumn (опционально): указывает столбец в таблице базы данных, где будет храниться информация о типе записи относительно иерархии классов.

  • @DiscriminatorMap (опционально): определяет соответствие значений в столбце, указанном в @DiscriminatorColumn, с конкретными типами записей.

Таким образом, при использовании SingleTable вся информация о полях различных классов хранится в одной таблице, и тип каждой записи определяется значением в дискриминаторном столбце. Это упрощает выборку данных и уменьшает необходимость использования операций JOIN.

Пример родительского класса из нашего кейса:

Пример дочернего класса Buggy:

И класса Car:

После миграции у нас появляется одна таблица auto, которая содержит все поля дочерних классов, а также добавляется поле дискриминатора, название которого мы указали в аннотации. Для фильтрации данных по этому полю можно использовать следующую конструкцию:

$query
  ->andWhere('Auto INSTANCE OF :type_auto')
  ->setParameter('type_auto', $request->get('filter')['type']);

Выбирая тип наследования, мы оперировали следующими критериями: скорость выполнения запросов и уникальный первичный ключ, получаемый в результате наследования.

Плюсы

  • Нет необходимости переписывать обработку существующих классов.

  • Снижение рисков возникновения конфликтов, поскольку первичный ключ один.

  • Выборка может осуществляться из дочернего и родительского классов.

  • Более быстрая обработка запросов в СУБД за счет отсутствия JOIN-операций и нескольких таблиц в запросе.

Минусы

  • Потенциально большой размер таблицы при наличии множества дочерних классов.

  • Требуется большой объем памяти для хранения таблицы на диске из-за большого количества полей, которые существуют только у определенных сущностей и отсутствуют у других. Количество таких полей возрастает с увеличением числа родственных сущностей.

Итог

Каждый из представленных типов наследования имеет свои плюсы и минусы. Чтобы выбрать правильное решение, необходимо тщательно оценить перспективы развития сущностей в вашем проекте, их количество, различия в полях, потребности в различных типах выборок.

После анализа всех возможных вариантов мы приняли решение использовать тип наследования SingleTable. За этим решением стояли следующие потребности и факторы:

  • Возможность обращения как к дочерним сущностям по отдельности, так и к общему списку (с постраничной выборкой, сортировкой, фильтрацией).

  • Небольшое количество родственных классов. Так мы не усложним код и исключим вероятность ошибок.

  • Ожидаемое количество записей, не превышающее нескольких сотен. Место, занимаемой таблицей на диске, не станет для нас проблемой.

  • Быстрая обработка запросов через ORM Doctrine.

  • Минимальный рефакторинг кода.

Таким образом, выбор наследования типа SingleTable обусловлен сочетанием удобства работы с данными, производительности запросов и минимальным вмешательством в существующий код.

© Habrahabr.ru