Иногда на практике бывает необходимо расширить сущности, передставляющие таблицы базы данных в коде. Например, PHP-фреймворк Symfony позволяет создавать сущности в так называемых вендорных бандлах. При необходимости в приложении, использующей этот бандл, можно расширить вендорную сущность свойствами, присущими предметной области данного приложения. В статье рассматриваются различные варианты расширения сущностей на примере Doctrine ORM с использованием языка PHP.
Начнем с рассмотрения простейшего случая расширения простым наследованием.
Для этого создадим сущность ParentEntity и расширим его сущностью ChildEntity.
В данном примере не будем использовать специальные аннотации Doctrine,
используемые для расширения сущностей.
<?php
// src/DataLayerBundle/Entity/ParentEntity.php
namespace DataLayerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="parent_entity")
*/
class ParentEntity
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
protected $name;
}
Сущность ParentEntity содержит идентификатор и свойсво $name,
которое является текстовым.
<?php
// src/AppBundle/Entity/ChildEntity.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use DataLayerBundle\Entity\ParentEntity;
/**
* @ORM\Table(name="child_entity")
*/
class ChildEntity extends ParentEntity
{
/**
* @var int
*
* @ORM\Column(name="name", type="integer")
*/
protected $name;
/**
* @var int
*
* @ORM\Column(name="some_int", type="integer")
*/
protected $someInt;
}
В дочерней сущности свойство $name базового класса переопределено
на целочисленное, также добавлено новое свойство $someInt.
Создадим файл миграции, используя созданные сущности.
$ php app/console doctrine:migrations:diff
// ...
$this->addSql('CREATE TABLE child_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, some_int INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE parent_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
// ...
Исходя из полученных запросов можно сделать следующие выводы:
@MappedSuperclassИзвестно, что Doctrine представляет функционал наследования сущностей с
использованием специальных аннотаций. В следующем примере попробуем использование
аннотации @MappedSuperclass в базовом классе.
<?php
// src/DataLayerBundle/Entity/ParentEntity.php
namespace DataLayerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\MappedSuperclass
*/
class ParentEntity
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
protected $name;
}
Дочерний класс ChildEntity оставим без изменений.
В результате в файле миграций будет запрос на создание только дочерней
таблицы. Стоит обратть внимание, что поле name в запросе имеет
тип INT, который был задан дочерним классом.
// ...
$this->addSql('CREATE TABLE child_entity (id INT NOT NULL, name INT NOT NULL, some_int INT NOT NULL, PRIMARY KEY(id))');
// ...
Напрашиваются следующие выводы:
А теперь попробуем удалить дочерний класс, оставив при этом аннотацию @MappedSuperclass в
базовом классе. Посмотрим, что будет в файле миграции? Как и следовало
ожидать, там нет запросов.
Паттерн «Наследование с единой таблицей» описан Мартиным Фаулером в книге Patterns of Enterprise Application Architecture.
Суть шаблона заключается в расположении полей нескольких классов в единой таблице СУБД. К примеру, это способствует уменьшению количества JOIN’ов при выборке данных из базы данных. Для реализации этого подхода нужно создать родительский класс и аннотировать его следующими аннотациями:
@InheritanceType - указывает тип наследования@DiscriminatorColumn (опционально) - указывает столбец в таблице
базы данных, в котором хранится информация о типе строки относительно иерархии
классов@DiscriminatorMap (опционально) - указывает, какой записью
в столбце @DiscriminatorColumn идентифицировать отпределнный
типВ исходном коде аннотации @InheritanceType видно, что она может
принимать значения: NONE, JOINED, SINGLE_TABLE, TABLE_PER_CLASS.
<?php
namespace Doctrine\ORM\Mapping;
/**
* @Annotation
* @Target("CLASS")
*/
final class InheritanceType implements Annotation
{
/**
* The inheritance type used by the class and its subclasses.
*
* @var string
*
* @Enum({"NONE", "JOINED", "SINGLE_TABLE", "TABLE_PER_CLASS"})
*/
public $value;
}
Для реализации данного паттерна следует использовать SINGLE_TABLE.
Создадим необходимые классы.
<?php
// src/DataLayerBundle/Entity/ParentEntity.php
namespace DataLayerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name = "discr", type = "string")
* @ORM\DiscriminatorMap({"parent_entity" = "ParentEntity", "child_entity" = "AppBundle\Entity\ChildEntity"})
*/
class ParentEntity
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
protected $name;
}
Дочерний класс ChildEntity оставим без изменений. В результате
в файле миграций появится запрос на создание только одной таблицы parent_entity.
// ...
$this->addSql('CREATE TABLE parent_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, discr VARCHAR(255) NOT NULL, some_int INT DEFAULT NULL, PRIMARY KEY(id))');
// ...
Видно, что:
Попробуем выполниь миграцию и выполнить ввод данных через наши сущности, например, в контроллере.
// ...
$em = $this->get('doctrine.orm.entity_manager');
$parent = new ParentEntity();
$parent->setName('parent name');
$child = new ChildEntity();
$child->setSomeInt(9999999);
$child->setName('child name');
$em->persist($parent);
$em->persist($child);
$em->flush();
// ...
В результате в таблице появятся следующие записи:
SELECT * FROM parent_entity;
| id | name | discr | some_int |
|---|---|---|---|
| 1 | parent name | parent_entity | |
| 2 | child name | child_entity | 9999999 |
Если не писать аннотацию @ORM\DiscriminatorMap({"parent_entity" = "ParentEntity", "child_entity" = "AppBundle\Entity\ChildEntity"}) в
родительском классе, то значения в столбеце discr будут следующими:
SELECT id, discr FROM parent_entity;
| id | discr |
|---|---|
| 1 | parententity |
| 2 | childentity |
Паттерн «Наследование с таблицами классов» также описан Мартиным Фаулером в книге Patterns of Enterprise Application Architecture.
В отличии от паттерна «Наследование с единой таблицей» в данном
подходе используется одна таблица на один класс в иерархии наследования.
Релизация отличается лишь значением аннотации @InheritanceType,
которым в данном случае является JOINED. Родительский класс
будет выглядеть следующим образом.
<?php
// src/DataLayerBundle/Entity/ParentEntity.php
namespace DataLayerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name = "discr", type = "string")
* @ORM\DiscriminatorMap({"parent_entity" = "ParentEntity", "child_entity" = "AppBundle\Entity\ChildEntity"})
*/
class ParentEntity
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
protected $name;
}
Дочерний класс ChildEntity снова оставим без изменений. В результате
создадутся следующие запросы в файле миграций.
// ...
$this->addSql('CREATE TABLE child_entity (id INT NOT NULL, some_int INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE parent_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, discr VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE child_entity ADD CONSTRAINT FK_677D8034BF396750 FOREIGN KEY (id) REFERENCES parent_entity (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
// ...
Таким образом, создаются две таблицы, по одрной на каждую сущность из
иерархии классов. В каждой таблице находятся только поля, объявленные в
соответсвующем классе сущности. Стоит обратить внимание а то, что создается
внешний ключ child_entity.id -> parent_entity.id. Произведем
ввод данных, аналогичный предыдущему, и сделаем выборку данных из таблиц.
SELECT * FROM parent_entity;
| id | name | discr |
|---|---|---|
| 1 | parent name | parent_entity |
| 2 | child name | child_entity |
SELECT * FROM child_entity;
| id | some_int |
|---|---|
| 2 | 9999999 |
В аннотации @InheritanceType используем значение TABLE_PER_CLASS.
Выполняем команду для создания файла миграций и получаем Doctrine\ORM\ORMException:
This behaviour is (currently) not supported by Doctrine 2
В документации Doctrine об этом типе ничего не сказано, но если
обратиться к документации Hibernate,
то можно сделать вывод, что должны создаваться таблицы для каждой сущности.
Причем каждая таблица содержит как свойства конкретной сущности, так и
свойства всех родителей. Судя по всему, тоже самое мы получим при аннотировании @InheritanceType("NONE").
$this->addSql('CREATE TABLE child_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, some_int INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE parent_entity (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');