Doctrine 2继承映射与关联

55

注意: 如果我想要的不可能实现,那么会接受“不可能”的答案

Doctrine 2关于继承映射的文档中,提到有两种方法:

  • 单表继承(STI)
  • 类表继承(CTI)

对于这两种方式都有一个警告:

如果您将STI / CTI实体用作多对一或一对一实体,则您永远不应该使用继承层次结构上层的任何类为“targetEntity”,只能使用没有子类的那些。否则Doctrine无法创建此实体的代理实例,并且将始终急切地加载该实体。

那么,我该如何使用基础(抽象)类的关联来进行继承呢?(并保持性能当然)


示例

用户拥有许多Pet(由DogCat扩展的抽象类)。

我想做的事:

class User {
    /**
     * @var array(Pet) (array of Dog or Cat)
     */
    private $pets;
}

由于Doctrine文档中的警告,我应该这样做:

class User {
    /**
     * @var array(Dog)
     */
    private $dogs;
    /**
     * @var array(Cat)
     */
    private $cats;
}

这很烦人,因为我失去了继承的好处!

注意:我没有添加用于映射到数据库的Doctrine注释,但您可以理解我的意思


好问题,你可能需要分别针对狗和猫进行定位。 - Hannes
2
是的,但那不是我想做的 :p - Matthieu Napoli
1
我也是 :-/ 我在节点(页面/帖子/内容)和评论方面有同样的情况。 - Hannes
2
请记住,这只是一个警告。我忽略了这个警告,因为看起来似乎没有解决方案。我接受在这些情况下它将不会加载代理。只是在技术概述和性能之间做出选择。我选择了前者...但仍然好奇是否有解决方案。 - Rene Terstegen
为什么你不能添加一个名为getPets的方法,将狗和猫合并并返回它们呢? - meze
显示剩余3条评论
2个回答

48

我感到有些疲倦,但这似乎是无事生非。

你错过了警告中的重要部分:

如果你将STI/CTI实体“作为多对一或一对一实体”使用

但在你的例子中并非如此!如果你没有省略Doctrine注释,你可能会注意到这一点。

User::pets关联是OneToMany而不是[One|Many]ToOne。一个用户拥有许多宠物。

反向关联确实是OneToOne,但它的目标是User,而User没有继承。

Robin的答案应该是一个很好的提示——你可以记录SQL查询并查看Doctrine实际上对你的数据库做了什么!


影响性能的情况类似于:

abstract class Pet { ... }

class Cat extends Pet { ... } 

class Dog extends Pet { ... }

class Collar {
   /**
    * @Column(length="16")
    */

   protected $color;
   /**
    * ManyToOne(targetEntity="Pet")
    */
   protected $owner;
}

如果你想要迭代所有蓝领工人,Doctrine 会遇到一些麻烦。因为它不知道 $owner 变量将会是哪个类,所以无法使用代理(Proxy)。相反,它被迫急切加载 $owner 变量,以找出它是一只猫还是一只狗。

对于 OneToMany 或 ManyToMany 的关系来说,这并不是问题,因为在这种情况下,延迟加载可以很好地工作。你不会得到一个代理(Proxy),而是得到一个 PersistentCollection(持久化集合)。而一个 PersistentCollection 总是只是一个 PersistentCollection,直到你实际请求它的内容,它才会关心它自己的内容。因此,延迟加载可以很好地工作。


哦!好的,恭喜你澄清了这一点,如果你确实是正确的(看起来是这样,我重新阅读了教义文档,你似乎是正确的),那么问题就解决了! - Matthieu Napoli
2
@timdev 你说“如果你想迭代所有蓝领”,那么“蓝色”部分很重要吗?如果我只想迭代所有领子,问题是否会消失? - marcv
我想补充一点,如果你在User::pets的拥有方使用了OneToMany,那么你应该在反向方使用ManyToOne注解。你所说的是实体的反向关系,而不是当前关系。 - Ruben

46

我认为你可能误解了,你引用的手册部分标题是“性能影响”,他们并没有告诉你不能这样做,只是在说明如果这样做会有性能方面的影响。 对于惰性加载来说,这是有道理的——对于 STI 实体的异构集合,您必须访问数据库并加载实体,才能知道它将属于哪个类,因此惰性加载不可能/没有意义。 我现在也在学习 Doctrine 2,所以我模拟了你的示例,以下更好地工作:

namespace Entities;

/**
 * @Entity
 * @Table(name="pets")
 * @InheritanceType("SINGLE_TABLE")
 * @DiscriminatorColumn(name="pet_type", type="string")
 * @DiscriminatorMap({"cat" = "Cat", "dog" = "Dog"})
 */
class Pet
{
    /** @Id @Column(type="integer") @generatedValue */
    private $id;

    /** @Column(type="string", length=300) */
    private $name;

    /** @ManyToOne(targetEntity="User", inversedBy="id") */
    private $owner;
}


/** @Entity */
class Dog extends Pet
{

    /** @Column(type="string", length=50) */
    private $kennels;
}

/** @Entity */
class Cat extends Pet
{
    /** @Column(type="string", length=50) */
    private $cattery;
}

/**
 * @Entity
 * @Table(name="users")
 */
class User
{

    /** @Id @Column(type="integer") @generatedValue */
    private $id;

    /** @Column(length=255, nullable=false) */
    private $name;


    /** @OneToMany(targetEntity="Pet", mappedBy="owner") */
    private $pets;
}

...和测试脚本...

if (false) {
    $u = new Entities\User;
    $u->setName("Robin");

    $p = new Entities\Cat($u, 'Socks');
    $p2 = new Entities\Dog($u, 'Rover');

    $em->persist($u);
    $em->persist($p);
    $em->persist($p2);
    $em->flush();
} else if (true) {
    $u = $em->find('Entities\User', 1);
    foreach ($u->getPets() as $p) {
        printf("User %s has a pet type %s called %s\n", $u->getName(), get_class($p), $p->getName());
    }
} else {
    echo "  [1]\n";
    $p = $em->find('Entities\Cat', 2);
    echo "  [2]\n";
    printf("Pet %s has an owner called %s\n", $p->getName(), $p->getOwner()->getName());
}

我所有的猫和狗都被正确地加载了:

如果你查看生成的 SQL,你会注意到当 OneToMany 的 targetEntity 是 "pet" 时,你会得到以下 SQL:

SELECT t0.id AS id1, t0.name AS name2, t0.owner_id AS owner_id3, pet_type, 
t0.cattery AS cattery4, t0.kennels AS kennels5 FROM pets t0 
WHERE t0.owner_id = ? AND t0.pet_type IN ('cat', 'dog')

但是当它设置为Cat时,你会得到这个:

SELECT t0.id AS id1, t0.name AS name2, t0.cattery AS cattery3, t0.owner_id 
AS owner_id4, pet_type FROM pets t0 WHERE t0.owner_id = ? AND t0.pet_type IN ('cat')

希望对你有所帮助。


我明白你的意思,但性能是非常重要的。我想知道我应该怎么做才能仍然利用继承而不受性能影响。如果这不可能(现在或永远与Doctrine),我希望看到它写在黑白上,因为对我来说似乎并不明显。那将是我的问题的答案。文档所说的是:使用那个解决方案,如果您想保持性能,则无法使用。但是否有任何方法可以绕过它? - Matthieu Napoli
我的问题虽然悬赏已经关闭,但仍然是开放的。 - Matthieu Napoli
1
大家好,看看timdev的回答,非常有趣。 - Matthieu Napoli
@Robin:看了你的代码,Pet类中的属性inversedBy="id"应该如何工作?这会引发Doctrine错误:"...引用了未定义为关联的反向侧字段Entity\User#id。"。 - webDEVILopers
@Web开发者们,我写下这个答案已经很长时间了,或许Doctrine在此期间有些微小的变化。不管怎样,我已经记不清楚它是怎么工作的了! - Robin
谢谢@Robin。我只是想知道你是否找到了一种神奇的方法来使用继承与基类(抽象类)的关联。因为我无法使其工作。 - webDEVILopers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接