Configure Doctrine Multiple target entities

Many developers are often plagued with questions of the best possible way to handle inheritance mapping or configuring multiple target entities in doctrine. How does one do that? Lets go over a slightly different scenario first to understand the real problem better.

Lets say you have 2 entities with their respective fields:

  • Person (id, name, profession)
  • Engineer (id, level)

We want that Person could have a profession of an Engineer. Hence the profession field (from Person entity) is supposed to hold a reference of the Engineer entity. How do we do that? Well, if you’re a bit familiar with doctrine relationships, its pretty simple. To define this association, we could use a ManyToOne relationship as shown below:


class Person {
// ...

    /**
    * Many Persons can have Engineering profession
    * @ORM\ManyToOne(targetEntity="Engineer")
    * @ORM\JoinColumn(name="engineer_id", referencedColumnName="id")
    */
    private $profession;
}


class Engineer {
    // ...
}

This would result in the creation of the following two tables:
1. Engineer Table Structure

Name Type Example Value
id int(11) 1
level varchar(255) Masters

2. Person Table Structure

Name Type Example Value
id int(11) 5
name varchar(255) John Doe
engineer_id int(11) 1

where engineer_id in the Person table is a foreign key to the id field of the Engineer table. And Viola! We can now have persons with the profession of Engineer. Simple huh?!


Lets get back to the real problem! What if we had another Entity Doctor

  • Doctor (id, type)

Doctor Table Structure

Name Type Example Value
id int(11) 1
type varchar(255) Dermatologist

We want to link this Doctor Entity to the Person Entity in such a way that the Profession could either be an Engineer or be a Doctor. In other words, the Person entity now needs a reference to both the Engineer and the Doctor entity.
We want something of the sort of @ORM\ManyToOne(targetEntity="Engineer, Doctor") but this annotation is Incorrect and NOT permitted in Doctrine. So how do we handle and define this relationship?

There are 2 ways to do it.


1. Single Table Inheritance to handle Multiple target Entities

This image gives a quick gist of Single Table Inheritance –
single Inheritance Table
The idea is that we use a single table to represent the inheritance structure in multiple tables. That means, all fields from different tables are mapped to a single table! This removes the need to use table joins. It is done using a special field(that can be named anything). This special field will have a value that is used to differentiate between multiple tables.
In the image above, you may have noticed, the “Players” table on the right side contains a type field that is used to differentiate. It could have the value player, footballer, cricketer or bowler representing the table/entity it belongs to. And very simply, problem solved!
To clarify further, lets apply this to our example where we want a Person’s profession to be either an Engineer or a Doctor.

// import dependencies
// use ...

/**
 * @ORM\Entity
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="profession_type", type="string")
 * @ORM\DiscriminatorMap({"profession_person" = "person", "profession_engineer" = "engineer", "profession_doctor" = "doctor"})
 */
class Person {
    // ... 

    /**
     * @ORM\Column
     */
    private $name;
}

class Engineer extends Person {
    // ...

    /**
     * @ORM\Column
     */
    private $level;
}

class Doctor extends Person {
    // ...

    /**
     * @ORM\Column
     */
    private $type;
}

As you can see, Person becomes the base class. Engineer and Doctor extend from this base class. Then, for the base class, InhertitanceType is set to SINGLE_TABLE via annotations.
The DiscriminatorColumn is the column that differentiates between different entities/tables. (We name it profession_type).
DiscriminatorMap annotation provides the values that should be used by the discriminator column to represent different tables. i.e. field profession_type can have either profession_person, profession_engineer or profession_doctor as its value (representing person, engineer and doctor respectively). The DiscriminatorMap annotation is optional and by default, doctrine would use the class name as the value for the DiscriminatorColumn.

Executing this doctrine relationship (php bin/console doctrine:schema:update --force) would result in the following Single table to be generated:
(You can also see the generated sql via php bin/console doctrine:schema:update --dump-sql)
Person Table Structure

Name Type Example Value
id int(11) 1
name varchar(255) John Doe
profession_type varchar(255) profession_doctor
level varchar(255) null
type varchar(255) Dermatologist

As you can see, now a person entity can have profession as a Doctor/Engineer as well as no profession. A few more examples will help you picture how this works:

id name profession_type level type
1 John Doe profession_person null null
2 Paul profession_engineer Masters null
3 Matthew profession_doctor null Ophthalmologist
 // import dependencies
 // use ...

 // add a Person with profession of a doctor
 $person1 = new Doctor();
 $person1->setName('Matthew');
 $person1->setType('Ophthalmologist');
 
 // get DB connection and flush
 $em = $this->getDoctrine()->getManager();
 $em->persist($person1);
 $em->flush();
 
 // now your DB should contain a new record (similar to #3 in above table)

Similarly, you should be able to add a Person with profession: Engineer. There is an alternate approach to handle our initial problem. It is achieved using Joined Table Inheritance. Single Table Inheritance and Joined Table Inheritance, both have its merits/demerits (discussed at the end of the article).


2. Joined/Class Table Inheritance to handle Multiple target Entities

Joined table Inheritance or Class Table Inheritance is a strategy where each class is mapped to its own table and its parent table.
An image to portray Joined table Inheritance –
Joined Inheritance Table

As you can see, each class has its own table and they will be linked together via joins. Doctrine implements this using a discriminator column for the base class.

/**
 * @ORM\Entity
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="profession_type", type="string")
 * @ORM\DiscriminatorMap({"profession_person" = "person", "profession_engineer" = "engineer", "profession_doctor" = "doctor"})
 */
class Person
{
    // ...
}


class Engineer extends Person
{
    // ...
}
class Doctor extends Person
{
    // ...
}

As you can see, code-wise, the only thing that changed is the value of InheritanceType in the annotations. It is now “JOINED” and so it results in 3 tables being created that are linked to each other via foreign-keys.

joined table inheritance

Now to add a Doctor, we could do this –

 // import dependencies
 // use ...

 // add a Person with profession of a doctor
 $person1 = new Doctor();
 $person1->setName('Matthew');
 $person1->setType('Ophthalmologist');
 
 // get DB connection and flush
 $em = $this->getDoctrine()->getManager();
 $em->persist($person1);
 $em->flush();
 

This will result in 1 row being inserted in the Person table [1, Matthew, profession_doctor] and 1 row being inserted in the Doctor table [1, Ophthalmologist]. Both will have the same id (i.e. 1) and that’s how they would be linked to each other.
This way, we can make the Person Entity to have the profession as either the Engineer/Doctor entity.


Merits and Demerits

Single Table Inheritance Joined/Class Table Inheritance
No Table Joins Needed Table Joins Needed
Adding new fields requires adding the new columns to the Base table Adding new fields requires adding the new tables
Efficient for quering since only a where clause is required Quering can have a performance impact since table joins are involved (specially for large tables)
For large deployments, adding new columns could have an adverse impact on the index/column layout inside the database Not applicable (since we add new tables instead of adding columns in the base table)

As seen, both the approaches have its advantages/disadvantages. Depending upon your requirement, you could choose which approach is apt for you.
That’s all folks! Hope it helps 🙂


References

Leave a Reply