Создание одного-много полиморфных отношений с доктриной
Позвольте мне начать с описания сценария. У меня есть объект Note, который может быть назначен для множества разных объектов
- A Книга может содержать одну или несколько Примечание.
- Изображение может содержать одну или несколько Примечание.
- A Адрес может иметь одну или несколько Примечание.
Что я вижу в базе данных:
книга
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
Вопрос в том, как я могу моделировать это внутри Doctrine. Все, что я прочитал о разговорах о наследовании отдельных таблиц, не имеющих отношения "один-два".
Чтобы отметить (каламбур не предназначен;), вы можете заметить, что заметке никогда не нужны уникальные атрибуты на основе связанного с объектом объекта.
В идеале я могу сделать $address->getNotes()
, который вернет тот же тип объекта Note, который вернет $image->getNotes()
.
Как минимум проблема, которую я пытаюсь решить, состоит в том, чтобы избежать трех разных таблиц: image_notes, book_notes и address_notes.
Ответы
Ответ 1
Лично я бы не использовал суперкласс здесь. Подумайте, что для интерфейса, который будет реализован с помощью 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
, идущие в другую сторону, только один из них применим для каждого экземпляра сущности:
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
, используя одностраничное наследование. Хотя теперь это может показаться более аккуратным, причина, по которой я бы не рекомендовал это, по мере роста вашей модели данных, может потребоваться ввести аналогичные шаблоны, которые в конечном итоге могут привести к тому, что дерево наследования станет неуправляемым.
РЕДАКТИРОВАТЬ: Было бы полезно иметь валидатор 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");
}
}
Ответ 2
Этот вопрос вводит излишнюю сложность в приложение. Просто потому, что ноты имеют одинаковую структуру, это не означает, что они являются одним и тем же объектом. При моделировании базы данных в 3NF они не являются одной и той же сущностью, потому что заметку нельзя перенести из книги в адрес. В вашем описании есть определенная связь между родителем и ребенком между книгой и book_note и т.д., Поэтому моделируйте ее как таковую.
Больше таблиц не проблема для базы данных, но сложность ненужного кода, как показывает этот вопрос. Это просто умение ради щебета. Это проблема с ORM, люди перестают выполнять полную нормализацию и не моделируют базу данных правильно.
Ответ 3
Самое близкое, что вы можете получить, это использовать однонаправленный One-To-Many с таблицей соединений. В доктрине это делается с помощью однонаправленного сообщения "многие ко многим" с уникальным ограничением для столбца соединения.
Недостаток этого решения заключается в том, что оно является однонаправленным, что означает, что ваша заметка не осведомлена о стороне-владельце отношений. Но на первый взгляд кажется, что это не должно быть проблемой в вашем случае.
Вы потеряете столбцы 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)}
* )
*/
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;
/**
* @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)}
* )
*/
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;
/**
* @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)}
* )
*/
protected $notes;
/**
* Constructor
*/
public function __construct()
{
$this->notes = new ArrayCollection();
}
}
Ответ 4
Я добавлю еще один ответ после прочтения вашего вопроса еще раз и осознаю, что вы заявили: "Я хочу избежать добавления другой таблицы соединений".
Создайте базовый объект Note
и продолжите этот класс в трех разных элементах примечания, например: 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
из-за ограничений внешнего ключа.
A 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();
}
}
Ответ 5
Поскольку у вас нет нескольких внешних ключей в одном столбце, у вас есть 2 решения:
- либо сделать односторонние отношения
one2many
между книгой/изображением/адресом и записью, поэтому она создаст таблицу объединений для каждого отношения, такого как book_note, image_note и т.д., удерживая пары id book.id-note.id и т.д. ( Я имею в виду один много, а не многие, потому что обычно нет смысла записывать заметку, какую книгу или изображение или адрес принадлежит ей)
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-many-unidirectional-with-join-table
-
или сделать несколько объектов BookNote
, ImageNote
, AddressNote
связанными с книгой/изображением/адресом и запиской и другими данными, если, например, у одного типа заметки есть другие данные.
-
вы можете использовать однонаправленное наследование, как описано в принятом ответе на этот вопрос, но это решение похоже на одно из предложенных Стивеном, что заставляет вас хранить дополнительные данные в столбце дискриминатора.
Несколько JoinColumns в Symfony2 с помощью аннотаций Doctrine?
Документация Doctrine также упоминает об ударе производительности в определенных ситуациях.
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#performance-impact
Решение, данное Стивом Чемберсом, решает проблему, но вы вынуждаете таблицу заметок хранить ненужные данные (пустые столбцы id для других объектов)