让我从概述这个场景开始。我有一个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
, Image
和Address
实现:
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
, 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
。
带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();
}
}
由于不能在一列上有多个外键,因此有两种解决方案:
- 在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_id
和object_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();
}
}