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

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

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

НТН.

Другие вопросы по тегам