Подтвердить что ты не робот

Доктрина 2 Наследование с ассоциацией

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

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

  • Наследование отдельных таблиц (STI)
  • Наследование таблицы классов (CTI)

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

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

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


Пример

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

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

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 для отображения в DB, ​​но вы можете понять, что я имею в виду

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ Robin должен был быть хорошим советом - вы можете записывать sql-запросы и видеть, какая доктрина действительно делает с вашей базой данных!


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

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, поэтому он не может использовать прокси. Вместо этого он вынужден охотно загружать $owner, чтобы узнать, является ли это кошкой или собакой.

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

Ответ 2

Я думаю, что вы неправильно поняли, раздел руководства, который вы указали, озаглавлен "Эффективное воздействие", они не говорят вам, что вы не можете этого сделать, только если есть последствия для производительности. Это имеет смысл для ленивой загрузки - для гетерогенных коллекций объектов 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;
}

... и тест script....

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 является "домашним животным", вы получаете 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')

НТН.