与学说建立一对多的多态关系


Creating a one to many polymorphic relationship with doctrine

让我从概述这个场景开始。我有一个Note对象,它可以被分配给许多不同的对象

  • Book可以有一个或多个Notes。
  • 一个图像可以有一个或多个注释
  • 一个地址可以有一个或多个

我设想的数据库的样子:

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

id | full      | thumb
2  | image.jpg | image-thumb.jpg
<<p> 地址/strong>
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中建模。我所读到的所有内容都是关于没有一对多关系的单表继承。

要注意(没有双关语),您可能会注意到note从不需要基于其相关对象的唯一属性。

理想情况下,我可以做$address->getNotes(),它将返回$image->getNotes()将返回的相同类型的Note对象。

在最小的问题,我试图解决是避免有三个不同的表:image_notes, book_notes和address_notes。

这个问题给应用程序带来了不必要的复杂性。仅仅因为这些音符有相同的结构并不意味着它们是同一个实体。在用3NF对数据库建模时,它们不是同一个实体,因为笔记不能从Book移动到Address。在你的描述中,book和book_note等之间存在明确的亲子关系,因此可以这样建模。

对于数据库来说,更多的表不是问题,但不必要的代码复杂性是问题,正如这个问题所演示的那样。这只是为了聪明而聪明。这就是orm的问题所在,人们停止了完全的规范化,也没有正确地为数据库建模。

我个人不会在这里使用超类。认为有更多的情况下,接口将由Book, ImageAddress实现:

interface iNotable
{
    public function getNotes();
}

下面是Book的例子:

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超类。虽然现在这样看起来比较整洁,但我不推荐这样做的原因是,随着数据模型的增长,您可能需要引入类似的模式,这些模式最终会使继承树变得难以管理。

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");
    }
}

在再次阅读您的问题并意识到您声明"我想避免添加另一个连接表"后,我添加了另一个答案。

创建一个Note基本实体,并在3个不同的音符实体中扩展该类,例如:BookNote, AddressNoteImageNote。在我的解决方案中,注释表看起来像这样:

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

带setter和getter的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();
    }
}

由于不能在一列上有多个外键,因此有两种解决方案:

  1. 在Book/Image/Address和Note之间建立单边one2many关系,因此它将为每个关系创建一个连接表,如book_note, image_note等,保存id对Book .id- Note。id等(我的意思是one2many而不是many2many,因为通常情况下,笔记没有必要知道它属于哪本书、哪张图片或哪一个地址)

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

  • 或使多个实体BookNote, ImageNote, AddressNote与书籍/图像/地址,笔记和其他数据相关,例如一种类型的笔记中有一些其他数据。

  • 您可以使用单表继承,正如对这个问题的公认答案所描述的那样,但它是一个类似于Steven提出的解决方案,它使您在discriminator列中保存额外的数据。

  • 多个JoinColumns在Symfony2使用学说注释?

    Doctrine文档还提到在某些情况下会影响性能。

    http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html性能的影响

    Steve Chambers给出的解决方案解决了一个问题,但是你强迫note表保存不必要的数据(其他对象的空id列)

    你可以得到的最接近的是通过使用单向一对多连接表。理论上,这是通过单向的多对多来完成的,并且在连接列上有一个唯一的约束。

    该解决方案的缺点是它是单向的,这意味着您的注释不知道关系的拥有方。但乍一看,这对你来说应该不是问题。您将丢失注释表中的object_idobject_type列。

    完整代码应该是这样的:

    带setter和getter的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();
        }
    }