与doctrine创建一对多的多态关系

时间:2015-10-08 23:37:16

标签: php mysql orm doctrine-orm doctrine

首先让我概述一下这个场景。我有一个可以分配给许多不同对象的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

问题是,我如何在Doctrine中对此进行建模。我读到的关于单表继承的讨论,没有一对多的关系。

要注意(没有双关语;),您可能会注意到基于与其相关的对象,笔记永远不需要唯一属性。

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

至少我要解决的问题是避免使用三个不同的表:image_notes,book_notes和address_notes。

5 个答案:

答案 0 :(得分:4)

我个人不会在这里使用超类。认为BookImageAddress可以实现接口的更多案例:

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或{{1}确切地确定$image验证器确保数据完整性也很有用为每个实例设置。像这样:

$address

答案 1 :(得分:4)

这个问题为应用程序带来了不必要的复杂性。仅仅因为音符具有相同的结构并不意味着它们是同一个实体。在3NF中对数据库建模时,它们不是同一个实体,因为无法将笔记从书籍移动到地址。在您的描述中,book和book_note等之间存在确定的父子关系,因此请对其进行建模。

对于数据库而言,更多的表不是问题,但正如此问题所示,不必要的代码复杂性。聪明的人只是为了聪明。这是ORM的问题,人们不再进行完全规范化,也不能正确建模数据库。

答案 2 :(得分:2)

我再次阅读你的问题之后又添加了另一个答案,并意识到你说过“我想避免添加另一个连接表”

创建一个Note基础实体,并在3个不同的注释实体中扩展此类,例如:BookNoteAddressNoteImageNote。在我的解决方案中,注释表将如下所示:

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

答案 3 :(得分:1)

由于你不能在一列上有几个外键,你有2个solutuons:

  1. 在Book / Image / Address和Note之间建立单边one2many关系,因此它会为book_note,image_note等每个关系创建一个连接表,其中包含id对book.id-note.id等(我的意思是one2many而不是many2many,因为笔记通常没有必要知道它属于哪本书或图像或地址)。
  2. http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-many-unidirectional-with-join-table

    1. 或制作与图书/图片/地址相关的多个实体BookNoteImageNoteAddressNote以及注释和其他数据,例如,如果某种类型的笔记有其中的一些其他数据。

    2. 您可以使用单个表继承,如此问题的已接受答案中所述,但它是一个类似于Steven提出的解决方案,它使您在鉴别器列中保存其他数据。

    3. Multiple JoinColumns in Symfony2 using Doctrine annotations?

      Doctrine文档还提到了某些情况下的性能损失。

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

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

答案 4 :(得分:1)

最接近的方法是使用带有连接表的单向一对多。 In doctrine this is done with a unidirectional Many-To-Many with a unique constraint on the join column

这种解决方案的缺点是它是单向的,意味着你的笔记不知道关系的拥有方。但乍一看,在您的情况下看起来这应该不是问题。 您将丢失注释表中的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)}
     * )
     */
    $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();
    }
}