55

NOTE : if what I want is not possible, a "not possible" answer will be accepted

In the Doctrine 2 documentation about inheritance mapping, it says there are 2 ways :

  • Single table inheritance (STI)
  • Class table inheritance (CTI)

For both, there is the warning :

If you use a STI/CTI entity as a many-to-one or one-to-one entity you should never use one of the classes at the upper levels of the inheritance hierachy as “targetEntity”, only those that have no subclasses. Otherwise Doctrine CANNOT create proxy instances of this entity and will ALWAYS load the entity eagerly.

So, how can I proceed to use inheritance with an association to the base (abstract) class ? (and keep the performance of course)


Example

A user has many Pet (abstract class extended by Dog or Cat).

What I want to do :

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

Because of the warning in Doctrine documentation, I should do that :

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

This is annoying, because I loose the benefits of inheritance !

Note : I didn't add the Doctrine annotations for the mapping to DB, but you can understand what I mean

Matthieu Napoli
  • 48,448
  • 45
  • 173
  • 261
  • good question, you probably have to target dogs and cats spereratly – Hannes Apr 19 '11 at 12:04
  • 2
    Yes, but that's not what I want to do :p – Matthieu Napoli Apr 19 '11 at 12:31
  • 1
    same here :-/ I've the same situation with nodes (pages / posts / stuff) and comments – Hannes Apr 19 '11 at 12:39
  • 2
    Remember it is just a warning. I ignored this warning cause it looks like there is no solution for it. I accepted that in these cases it will not load the proxies. Just a choice between technical overview and performance. I choose the first... But still curious if there is a solution for this somewhere. – Rene Terstegen Apr 19 '11 at 13:35
  • I must be missing something, but I'd love to help you. Can you possibly expand on your question to include examples of what you *want* to do vs. what you feel like you *have* to do? As the question is written now it seems like it would require someone to be in the same mindset as a typical Doctrine user, which I am not :) – Kevin Peno Apr 25 '11 at 19:50
  • I extended the example to be clearer, let me know if that's better. Thanks – Matthieu Napoli Apr 26 '11 at 11:13
  • why can't you just add a method getPets that will merge dogs and cats and return them? – meze Apr 26 '11 at 11:51
  • That's not a bad idea, though it's some work that inheritance is supposed to handle :(. And if I want to add pets, I have to have some switch or ifs with `instanceof`... Lot's of weird stuff for such a simple thing. I'll keep that in mind if no solution comes up. – Matthieu Napoli Apr 26 '11 at 11:54

2 Answers2

48

I'm tired, but this seems like much ado about nothing.

You missed the important bit of that warning:

If you use a STI/CTI entity as a many-to-one or one-to-one entity

That's not the case in your example! If you had not omitted the doctrine annotations, you might have noticed.

The association User::pets is OneToMany, not [One|Many]ToOne. One user has many pets.

The inverse association is OneToOne, but it's targeting User, which has no inheritance.

Robin's answer should have been a good hint -- you can log the sql queries and see what doctrine actually does to your database!


The bad-for-performance scenario is something like:

abstract class Pet { ... }

class Cat extends Pet { ... } 

class Dog extends Pet { ... }

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

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

Now, if you wanted to iterate over all the blue collars, Doctrine runs into some trouble. It doesn't know what class $owner is going to be, so it can't use a Proxy. Instead, it's forced to eagerly load $owner to find out whether it's a Cat or a Dog.

This isn't a problem for OneToMany or ManyToMany relationships, because in that case, lazy loading works fine. Instead of a proxy, you get a PersistentCollection. And a PersistentCollection is always just a PersistentCollection. It doesn't care about it's own contents until you actually ask for them. So lazy loading works fine.

timdev
  • 61,857
  • 6
  • 82
  • 92
  • Oh! Well congrats for clarifying that, if you are indeed right (it seems so, I'v re-read the doctrine documentation and you seem to be right), then the problem is solved! – Matthieu Napoli Aug 18 '11 at 11:25
  • 2
    @timdev You say "_if you wanted to iterate over all the blue collars_". Is the "blue" part important? Does the problem disappear if I just want to iterate over **all** collars? – marcv May 27 '15 at 07:28
  • I would like to add, that if your owning side of the User::pets is ```OneToMany```, you should use the ```ManyToOne``` annotation on the inverse side. You are 'talking' about the inverse side of the Entity, not the current relation of the Entity. – Ruben Jun 30 '15 at 12:51
46

I think you've misunderstood, the section of the manual you've quoted is entitled "Performance impact", they're not telling you you can't do this, only that there are performance implications if you do. This makes sense for lazy loading - for heterogeneous collections of STI entities you have to go to the database and load the entity before you know what class it will be, so lazy loading isn't possible / doesn't make sense. I'm learning Doctrine 2 myself at the moment, so I mocked up your example, the following works OK for more:

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

... and the test 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());
}

All my cats and dogs load as the correct type:

If you look at the generated SQL, you'll notice that when the OneToMany targetEntity is "pet", you get SQL like this:

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

But when it's set to Cat, you get this:

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.

Robin
  • 4,242
  • 1
  • 20
  • 20
  • I see what you mean, but performance is a great deal. I'm wondering what should I do to still take advantage of inheritance and don't suffer from the performance impact. If that's not possible (for now, or for ever with Doctrine), I want to see it written black on white because it doesn't seem obvious to me. That would be an answer to my question. What the doc is saying is : with that solution, you can't if you want to keep performance. But is there any way to go around that ? – Matthieu Napoli Apr 27 '11 at 07:57
  • My question is still open even though the bounty is closed – Matthieu Napoli Apr 29 '11 at 15:11
  • 1
    To everyone, see timdev's answer, very interesting. – Matthieu Napoli Aug 18 '11 at 11:26
  • @Robin : Looking at your code how is the attribute `inversedBy="id"` in your Pet class suppose to work? This would throw a Doctrine error: "...refers to the inverse side field Entity\User#id which is not defined as association.". – webDEVILopers Nov 19 '14 at 15:07
  • @webDEVILopers It's a long time since I wrote this answer, perhaps Doctrine has changed subtly since. Either way, I don't remember exactly how this works! – Robin Nov 21 '14 at 17:19
  • Thanks @Robin. I just wondered if you found a magic way to use inheritance with an association to the base (abstract) class. Because I could not make it work. – webDEVILopers Nov 23 '14 at 00:06