web-dev-qa-db-fra.com

Doctrine 2 Cartographie d'héritage avec association

REMARQUE: si ce que je veux n'est pas possible, une réponse "impossible" sera acceptée

Dans la documentation Doctrine 2 sur le mappage d'héritage , il est dit qu'il y a 2 façons:

  • Héritage de table unique (STI)
  • Héritage de table de classe (CTI)

Pour les deux, il y a l'avertissement:

Si vous utilisez une entité STI/CTI en tant qu'entité plusieurs-à-un ou un-à-un , vous ne devez jamais utiliser l'une des classes aux niveaux supérieurs de la hiérarchie d'héritage en tant que "targetEntity", uniquement ceux qui n'ont pas de sous-classes. Sinon Doctrine NE PEUT PAS créer d'instances proxy de cette entité et chargera TOUJOURS l'entité avec impatience.

Alors, comment puis-je procéder pour utiliser l'héritage avec une association à la classe de base (abstraite) ? (et garder les performances bien sûr)


Exemple

Un utilisateur possède plusieurs Pet (classe abstraite étendue par Dog ou Cat).

Ce que je veux faire :

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

À cause de l'avertissement dans la documentation Doctrine, je dois le faire:

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

C'est ennuyeux, car je perds les avantages de l'héritage!

Remarque: je n'ai pas ajouté les Doctrine annotations pour le mappage vers la base de données, mais vous pouvez comprendre ce que je veux dire

55
Matthieu Napoli

Je suis fatigué, mais cela semble beaucoup de bruit pour rien.

Vous avez raté la partie importante de cet avertissement:

Si vous utilisez une entité STI/CTI en tant qu'entité plusieurs-à-un ou un-à-un

Ce n'est pas le cas dans votre exemple! Si vous n'aviez pas omis les annotations doctrine, vous l'avez peut-être remarqué.

L'association User :: pets est OneToMany, pas [One | Many] ToOne. Un utilisateur a plusieurs animaux de compagnie.

L'association inverse is OneToOne, mais elle cible l'utilisateur, qui n'a pas d'héritage.

La réponse de Robin aurait dû être un bon indice - vous pouvez enregistrer les requêtes SQL et voir ce que doctrine fait réellement dans votre base de données!


Le scénario mauvais pour les performances est quelque chose comme:

abstract class Pet { ... }

class Cat extends Pet { ... } 

class Dog extends Pet { ... }

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

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

Maintenant, si vous voulez parcourir tous les cols bleus, Doctrine rencontre des problèmes. Il ne sait pas quelle classe $ owner va être, donc il ne peut pas utiliser de proxy Au lieu de cela, il est obligé de charger $ propriétaire avec impatience pour savoir s'il s'agit d'un chat ou d'un chien.

Ce n'est pas un problème pour les relations OneToMany ou ManyToMany, car dans ce cas, le chargement paresseux fonctionne correctement. Au lieu d'un proxy, vous obtenez un PersistentCollection. Et une PersistentCollection n'est toujours qu'une PersistentCollection. Il ne se soucie pas de son propre contenu jusqu'à ce que vous le demandiez. Le chargement paresseux fonctionne donc très bien.

47
timdev

Je pense que vous avez mal compris, la section du manuel que vous avez citée est intitulée "Impact sur les performances", ils ne vous disent pas ne peut pas faire cela, seulement qu'il y a des implications sur les performances si tu fais. Cela a du sens pour le chargement paresseux - pour les collections hétérogènes d'entités STI, vous devez aller dans la base de données et charger l'entité avant de savoir de quelle classe il s'agit, donc le chargement paresseux n'est pas possible/n'a pas de sens. J'apprends Doctrine 2 moi-même en ce moment, donc j'ai simulé votre exemple, ce qui suit fonctionne bien pour en savoir plus:

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;
}

... et le script de test ....

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());
}

Tous mes chats et chiens se chargent comme le bon type:

Si vous regardez le SQL généré, vous remarquerez que lorsque OneToMany targetEntity est "pet", vous obtenez SQL comme ceci:

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')

Mais quand il est réglé sur Cat, vous obtenez ceci:

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')

HTH.

46
Robin