Позвольте мне начать с описания сценария. У меня есть объект 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 решения:
one2many
, поэтому она создаст таблицу соединений для каждого отношения, например book_note, image_note и т. д., где хранятся пары id book.id-note.id и т. д. (я имею в виду один, а не один many2many, потому что обычно нет смысла указывать, какую книгу или изображение или адрес принадлежит ей) или сделать несколько объектов BookNote
, ImageNote
, AddressNote
связанных с книгой / изображением / адресом и запиской и другими данными, если, например, у одного типа заметки есть другие данные.
вы можете использовать однонаправленное наследование таблицы, как описано в принятом ответе на этот вопрос, но это решение, подобное предложению Стивена, которое заставляет вас хранить дополнительные данные в столбце дискриминатора.
Несколько JoinColumns в Symfony2 с помощью аннотаций Doctrine?
Документация Doctrine также упоминает успех в определенных ситуациях.
Решение, данное Стивом Чемберсом, решает проблему, но вы вынуждаете таблицу заметок хранить ненужные данные (пустые столбцы id для других объектов)