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

Позвольте мне начать с описания сценария. У меня есть объект 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

  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 для других объектов)