Доктрина 2 Карта наследования с ассоциацией - PullRequest
54 голосов
/ 19 апреля 2011

ПРИМЕЧАНИЕ: если то, что я хочу, невозможно, ответ «невозможно» будет принят

В документации Doctrine 2 о отображении наследования говорится, что есть 2 способа:

  • Наследование в одной таблице (STI)
  • Таблица классов наследования (CTI)

Для обоих есть предупреждение:

Если вы используете объект STI / CTI в качестве объекта «многие к одному» или «один к одному» , вам никогда не следует использовать один из классов на верхних уровнях иерархии наследования в качестве «targetEntity». Только те, которые не имеют подклассов. В противном случае Doctrine НЕ МОЖЕТ создавать прокси-экземпляры этой сущности и ВСЕГДА будет загружать сущность с нетерпением.

Итак, как мне перейти к использованию наследования с ассоциацией к базовому (абстрактному) классу ? (и сохраняем производительность конечно)


Пример

У пользователя много Pet (абстрактный класс расширен на Dog или Cat).

Что я хочу сделать:

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

Из-за предупреждения в документации Учения я должен сделать это:

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

Это раздражает, потому что я теряю преимущества наследства!

Примечание. Я не добавил аннотации Doctrine для сопоставления с БД, но вы можете понять, что я имею в виду

Ответы [ 2 ]

47 голосов
/ 18 августа 2011

Я устал, но это похоже на много шума из ничего.

Вы пропустили важный бит этого предупреждения:

Если вы используете объект STI / CTI как сущность «многие к одному» или «один к одному»

Это не так в вашем примере!Если вы не пропустили аннотации к доктрине, вы могли бы заметить.

Ассоциация User :: pets - это OneToMany, а не [One | Many] ToOne.У одного пользователя много домашних животных.

Обратная связь - это OneToOne, но она нацелена на пользователя, у которого нет наследования.

Ответ Робина должен был быть хорошим намеком - вы можете регистрировать SQL-запросы.и посмотрите, что на самом деле делает доктрина с вашей базой данных!


Сценарий с плохой производительностью выглядит примерно так:

abstract class Pet { ... }

class Cat extends Pet { ... } 

class Dog extends Pet { ... }

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

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

Теперь, если вы хотите перебрать все синиеОшейники, Доктрина сталкивается с некоторыми неприятностями.Он не знает, каким будет класс $ owner, поэтому он не может использовать прокси.Вместо этого он вынужден загружать $ owner, чтобы выяснить, является ли он котом или собакой.

Это не проблема для отношений OneToMany или ManyToMany, потому что в этом случае ленивая загрузка работает нормально.Вместо прокси вы получаете PersistentCollection.И PersistentCollection всегда является просто PersistentCollection.Это не заботится о его собственном содержании, пока вы на самом деле не попросите их.Так что ленивая загрузка работает нормально.

46 голосов
/ 27 апреля 2011

Я думаю, что вы не поняли, раздел руководства, который вы цитировали, озаглавлен «Влияние на производительность», вам не говорят, что не может сделать это, только что есть производительность последствия, если вы делаете. Это имеет смысл для отложенной загрузки - для разнородных коллекций объектов 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, вы заметите, что когда targetEntity OneToMany имеет значение «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')

НТН.

...