Создание одного-много полиморфных отношений с доктриной

Позвольте мне начать с описания сценария. У меня есть объект Note, который может быть назначен для множества разных объектов

  • Книга может иметь один или несколько примечаний .
  • Изображение может иметь один или несколько примечаний .
  • Адрес может иметь один или несколько примечаний .

Что я вижу в базе данных:

книга

id | title | pages 1 | harry potter | 200 2 | game of thrones | 500 

образ

 id | full | thumb 2 | image.jpg | image-thumb.jpg 

адрес

 id | street | city 1 | 123 street | denver 2 | central ave | tampa 

заметка

 id | object_id | object_type | content | date 1 | 1 | image | "lovely pic" | 2015-02-10 2 | 1 | image | "red tint" | 2015-02-30 3 | 1 | address | "invalid" | 2015-01-05 4 | 2 | book | "boobies" | 2014-09-06 5 | 1 | book | "prettygood" | 2016-05-05 

Вопрос в том, как мне смоделировать это внутри Доктрины. Все, что я прочитал о разговорах о наследовании отдельных таблиц, не имеющих отношения «один-два».

Чтобы отметить (каламбур не предназначен;), вы можете заметить, что заметке никогда не нужны уникальные атрибуты на основе связанного с объектом объекта.

В идеале я могу сделать $address->getNotes() который вернет тот же тип объекта Note, который будет возвращен $image->getNotes() .

Как минимум проблема, которую я пытаюсь решить, состоит в том, чтобы избежать трех разных таблиц: image_notes, book_notes и address_notes.

Лично я бы не стал использовать суперкласс. Подумайте, что есть еще один пример интерфейса , который будет реализован в Book , Image и Address :

 interface iNotable { public function getNotes(); } 

Вот пример книги:

 class Book implements iNotable { /** * @OneToMany(targetEntity="Note", mappedBy="book") **/ protected $notes; // ... public function getNotes() { return $this->notes; } } 

Note тогда необходимо, @ManyToOne отношения @ManyToOne проходили другим способом, только один из них будет применяться для каждого экземпляра объекта:

 class Note { /** * @ManyToOne(targetEntity="Book", inversedBy="notes") * @JoinColumn **/ protected $book; /** * @ManyToOne(targetEntity="Image", inversedBy="notes") * @JoinColumn **/ protected $image; /** * @ManyToOne(targetEntity="Address", inversedBy="notes") * @JoinColumn **/ protected $address; // ... } 

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

EDIT: было бы полезно иметь средство проверки Note для обеспечения целостности данных, установив, что для каждого экземпляра задается ровно один из $book , $image или $address . Что-то вроде этого:

 /** * @PrePersist @PreUpdate */ public function validate() { $refCount = is_null($book) ? 0 : 1; $refCount += is_null($image) ? 0 : 1; $refCount += is_null($address) ? 0 : 1; if ($refCount != 1) { throw new ValidateException("Note references are not set correctly"); } } 

Этот вопрос вводит излишнюю сложность в приложение. Просто потому, что ноты имеют одинаковую структуру, это не означает, что они являются одним и тем же объектом. При моделировании базы данных в 3NF они не являются одной и той же сущностью, потому что заметку нельзя перенести из книги в адрес. В вашем описании есть определенная связь между родителем и ребенком между книгой и book_note и т. Д., Поэтому моделируйте ее как таковую.

Больше таблиц не проблема для базы данных, но сложность ненужного кода, как показывает этот вопрос. Это просто умение ради щебета. Это проблема с ORM, люди перестают выполнять полную нормализацию и не правильно моделируют базу данных.

Я добавлю еще один ответ, снова прочитав ваш вопрос и осознав, что вы заявили: «Я хочу избежать добавления другой таблицы соединений» .

Создайте базовый объект Note и продолжите этот класс в трех разных BookNote примечаний, например: BookNote , AddressNote и ImageNote . В моем решении таблица примечаний будет выглядеть так:

 id | address_id | book_id | image_id | object_type | content | date 1 | null | null | 1 | image | "lovely pic" | 2015-02-10 2 | null | null | 1 | image | "red tint" | 2015-02-30 3 | 1 | null | null | address | "invalid" | 2015-01-05 4 | null | 2 | null | book | "boobies" | 2014-09-06 5 | null | 1 | null | book | "prettygood" | 2016-05-05 

Вы не можете использовать один общий столбец object_id из-за ограничений внешнего ключа.

NotesTrait с сеттерами и геттерами для заметок для предотвращения дублирования кода:

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; /** * @property Collection $notes */ trait NotesTrait { /** * Add note. * * @param Note $note * @return self */ public function addNote(Note $note) { $this->notes[] = $note; return $this; } /** * Add notes. * * @param Collection $notes * @return self */ public function addNotes(Collection $notes) { foreach ($notes as $note) { $this->addNote($note); } return $this; } /** * Remove note. * * @param Note $note */ public function removeNote(Note $note) { $this->notes->removeElement($note); } /** * Remove notes. * * @param Collection $notes * @return self */ public function removeNotes(Collection $notes) { foreach ($notes as $note) { $this->removeNote($note); } return $this; } /** * Get notes. * * @return Collection */ public function getNotes() { return $this->notes; } } 

Ваш объект Note :

 <?php namespace Application\Entity; /** * @Entity * @InheritanceType("SINGLE_TABLE") * @DiscriminatorColumn(name="object_type", type="string") * @DiscriminatorMap({"note"="Note", "book"="BookNote", "image"="ImageNote", "address"="AddressNote"}) */ class Note { /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * @var string * @ORM\Column(type="string") */ protected $content /** * @var string * @ORM\Column(type="date") */ protected $date } 

Ваш BookNote :

 <?php namespace Application\Entity; /** * @Entity */ class BookNote extends Note { /** MANY-TO-ONE BIDIRECTIONAL, OWNING SIDE * @var Book * @ORM\ManyToOne(targetEntity="Application\Entity\Book", inversedBy="notes") * @ORM\JoinColumn(name="book_id", referencedColumnName="id", nullable=true) */ protected $book; } 

Ваш AddressNote :

 <?php namespace Application\Entity; /** * @Entity */ class AddressNote extends Note { /** MANY-TO-ONE BIDIRECTIONAL, OWNING SIDE * @var Address * @ORM\ManyToOne(targetEntity="Application\Entity\Address", inversedBy="notes") * @ORM\JoinColumn(name="address_id", referencedColumnName="id", nullable=true) */ protected $address; } 

Ваш ImageNote :

 <?php namespace Application\Entity; /** * @Entity */ class ImageNote extends Note { /** MANY-TO-ONE BIDIRECTIONAL, OWNING SIDE * @var Image * @ORM\ManyToOne(targetEntity="Application\Entity\Image", inversedBy="notes") * @ORM\JoinColumn(name="image_id", referencedColumnName="id", nullable=true) */ protected $image; } 

Ваша Book :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Book { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** ONE-TO-MANY BIDIRECTIONAL, INVERSE SIDE * @var Collection * @ORM\OneToMany(targetEntity="Application\Entity\BookNote", mappedBy="book") */ protected $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Ваш Address :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Address { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** ONE-TO-MANY BIDIRECTIONAL, INVERSE SIDE * @var Collection * @ORM\OneToMany(targetEntity="Application\Entity\AddressNote", mappedBy="address") */ protected $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Ваш объект Image :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Image { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** ONE-TO-MANY BIDIRECTIONAL, INVERSE SIDE * @var Collection * @ORM\OneToMany(targetEntity="Application\Entity\ImageNote", mappedBy="image") */ protected $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Самое близкое, что вы можете получить, это использовать однонаправленный «Один-ко-многим» с таблицей соединений. В доктрине это делается с помощью однонаправленного «Много-ко-многим» с уникальным ограничением на столбец объединения .

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

В полном коде это будет выглядеть так:

NotesTrait с сеттерами и геттерами для заметок для предотвращения дублирования кода:

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; /** * @property Collection $notes */ trait NotesTrait { /** * Add note. * * @param Note $note * @return self */ public function addNote(Note $note) { $this->notes[] = $note; return $this; } /** * Add notes. * * @param Collection $notes * @return self */ public function addNotes(Collection $notes) { foreach ($notes as $note) { $this->addNote($note); } return $this; } /** * Remove note. * * @param Note $note */ public function removeNote(Note $note) { $this->notes->removeElement($note); } /** * Remove notes. * * @param Collection $notes * @return self */ public function removeNotes(Collection $notes) { foreach ($notes as $note) { $this->removeNote($note); } return $this; } /** * Get notes. * * @return Collection */ public function getNotes() { return $this->notes; } } 

Ваша Book :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Book { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * @ORM\ManyToMany(targetEntity="Note") * @ORM\JoinTable(name="book_notes", * inverseJoinColumns={@ORM\JoinColumn(name="note_id", referencedColumnName="id")}, * joinColumns={@ORM\JoinColumn(name="book_id", referencedColumnName="id", unique=true)} * ) */ $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Ваш Address :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Address { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * @ORM\ManyToMany(targetEntity="Note") * @ORM\JoinTable(name="address_notes", * inverseJoinColumns={@ORM\JoinColumn(name="note_id", referencedColumnName="id")}, * joinColumns={@ORM\JoinColumn(name="address_id", referencedColumnName="id", unique=true)} * ) */ $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Ваш объект Image :

 <?php namespace Application\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; class Image { use NotesTrait; /** * @var integer * @ORM\Id * @ORM\Column(type="integer", nullable=false) * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * @ORM\ManyToMany(targetEntity="Note") * @ORM\JoinTable(name="image_notes", * inverseJoinColumns={@ORM\JoinColumn(name="note_id", referencedColumnName="id")}, * joinColumns={@ORM\JoinColumn(name="image_id", referencedColumnName="id", unique=true)} * ) */ $notes; /** * Constructor */ public function __construct() { $this->notes = new ArrayCollection(); } } 

Поскольку у вас не может быть несколько внешних ключей на одном столбце, у вас есть 2 решения:

  1. либо сделайте односторонние отношения между книгой / изображением / адресом и one2many , поэтому она создаст таблицу соединений для каждого отношения, например book_note, image_note и т. д., где хранятся пары id book.id-note.id и т. д. (я имею в виду один, а не один many2many, потому что обычно нет смысла указывать, какую книгу или изображение или адрес принадлежит ей)

http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-many-unidirectional-with-join-table

  1. или сделать несколько объектов BookNote , ImageNote , AddressNote связанных с книгой / изображением / адресом и запиской и другими данными, если, например, у одного типа заметки есть другие данные.

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

Несколько JoinColumns в Symfony2 с помощью аннотаций Doctrine?

Документация Doctrine также упоминает успех в определенных ситуациях.

http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#performance-impact

Решение, данное Стивом Чемберсом, решает проблему, но вы вынуждаете таблицу заметок хранить ненужные данные (пустые столбцы id для других объектов)