Doctrine 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 для сопоставления с БД, но вы понимаете, что я имею в виду


person Matthieu Napoli    schedule 19.04.2011    source источник
comment
хороший вопрос, вам, вероятно, придется специально нацеливаться на собак и кошек   -  person Hannes    schedule 19.04.2011
comment
Да, но это не то, чем я хочу заниматься :p   -  person Matthieu Napoli    schedule 19.04.2011
comment
то же самое :-/ У меня такая же ситуация с нодами (страницами/постами/вещами) и комментариями   -  person Hannes    schedule 19.04.2011
comment
Помните, что это всего лишь предупреждение. Я проигнорировал это предупреждение, потому что похоже, что для него нет решения. Я согласился, что в этих случаях он не будет загружать прокси. Просто выбор между техническим обзором и производительностью. Я выбираю первое... Но все же любопытно, есть ли где-нибудь решение для этого.   -  person Rene Terstegen    schedule 19.04.2011
comment
Я должен что-то упустить, но я хотел бы помочь вам. Не могли бы вы расширить свой вопрос, включив в него примеры того, что вы хотите сделать по сравнению с тем, что, по вашему мнению, вы должны сделать? Поскольку вопрос написан сейчас, кажется, что для этого потребуется, чтобы кто-то придерживался того же мышления, что и типичный пользователь Doctrine, а я нет :)   -  person Kevin Peno    schedule 25.04.2011
comment
Я расширил пример, чтобы быть более ясным, дайте мне знать, если это лучше. Спасибо   -  person Matthieu Napoli    schedule 26.04.2011
comment
почему нельзя просто добавить метод getPets, который будет объединять собак и кошек и возвращать их?   -  person meze    schedule 26.04.2011
comment
Это неплохая идея, хотя наследование должно выполнять некоторую работу :(. И если я хочу добавить домашних животных, мне нужно иметь какой-то переключатель или если с instanceof... Много странных вещей для такой простой вещи. Буду иметь в виду, если решения не будет.   -  person Matthieu Napoli    schedule 26.04.2011


Ответы (2)


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

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

Если вы используете объект 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;
}

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

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

person timdev    schedule 18.08.2011
comment
Ой! Что ж, поздравляю с разъяснением того, что если вы действительно правы (кажется, что да, я перечитал документацию по доктрине, и вы, похоже, правы), то проблема решена! - person Matthieu Napoli; 18.08.2011
comment
@timdev Вы говорите если хотите перебрать всех синих воротничков. Синяя часть важна? Проблема исчезнет, ​​если я просто хочу перебрать все ошейники? - person marcv; 27.05.2015
comment
Я хотел бы добавить, что если ваша сторона User::pets OneToMany, вы должны использовать аннотацию ManyToOne на обратной стороне. Вы «говорите» об обратной стороне Сущности, а не о текущем отношении Сущности. - person Ruben; 30.06.2015

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

ХТН.

person Robin    schedule 26.04.2011
comment
Я понимаю, что вы имеете в виду, но производительность имеет большое значение. Мне интересно, что мне делать, чтобы по-прежнему пользоваться преимуществами наследования и не страдать от влияния на производительность. Если это невозможно (на данный момент или навсегда с Doctrine), я хочу, чтобы это было написано черным по белому, потому что мне это не кажется очевидным. Это был бы ответ на мой вопрос. Что говорит документ: с этим решением вы не можете, если хотите сохранить производительность. Но есть ли способ обойти это? - person Matthieu Napoli; 27.04.2011
comment
Мой вопрос все еще открыт, хотя награда закрыта - person Matthieu Napoli; 29.04.2011
comment
Всем смотрите ответ timdev, очень интересно. - person Matthieu Napoli; 18.08.2011
comment
@Robin: Глядя на ваш код, как должен работать атрибут inversedBy="id" в вашем классе Pet? Это вызовет ошибку Doctrine: ...относится к полю обратной стороны Entity\User#id, которое не определено как ассоциация.. - person webDEVILopers; 19.11.2014
comment
@webDEVILopers Прошло много времени с тех пор, как я написал этот ответ, возможно, с тех пор Doctrine немного изменилась. В любом случае, я точно не помню, как это работает! - person Robin; 21.11.2014
comment
Спасибо @Робин. Мне просто интересно, нашли ли вы волшебный способ использовать наследование с ассоциацией с базовым (абстрактным) классом. Потому что я не мог заставить его работать. - person webDEVILopers; 23.11.2014