About Doctrine inheritance
At a previous SymfonyLive conference (slides here, in French), I advised not to use Doctrine inheritance mapping. After a discussion with another developer last week, I realized the subject is worth an article with more details.
3 different flavors
Doctrine ORM offers 3 different type of inheritance mapping (reference documentation):
- Single Table Inheritance (STI)
- Class Table Inheritance (CTI)
- Mapped Superclass.
Let’s start with Single and Multiple Class Inheritance. Those types change database schema, let’s try to understand how data is stored.
Imagine we have 3 entities, a Blog Article
, ArticleWithPicture
and ArticleWithVideo
. Article
entity has a few properties, and each child has some additional properties.
On top of that, Doctrine requires the parent entity to have a “discriminator” column, which will contain the type of the entity (string). We call that one discr
, and we assume the class name in snake case is used.
UML schema of our example
Single Table Inheritance
Single Table Inheritance will map all those entities to the very same table in database. That table will have a column for every property of Article
, ArticleWithPicture
and ArticleWithVideo
, they may be eventually null
.
Here is an example of what the article
table could look like:
id | discr | title | content | picturePath | videoPath |
1 | article |
Hello world |
Lorem... |
null |
null |
2 | article_with_picture |
My first picture! |
Nec devio nec... |
romaric.jpg |
null |
3 | article_with_video |
My first video |
Aliquam et... |
null |
video.mp4 |
This option is not quite scalable. Entities with a dozen of properties will result in a huge database, and we could be concerned about “holes” in data. For instance, in the schema, picturePath
and videoPath
will always be nullable.
Class Table Inheritance
Here comes Class Table Inheritance. In that mode, entities will share a table for the parent data, plus one table for every child own data.
Let’s continue with previous example. We will have 3 tables:
- a
article
table (for the parent entity):
id | discr | title | content |
1 | article |
Hello world |
Lorem... |
2 | article_with_picture |
My first picture! |
Nec devio nec... |
3 | article_with_video |
My first video |
Aliquam et... |
- a
article_with_picture
table, with only one row. Notice thearticle_id
referring to the parent entity:
id | article_id | picturePath |
1 | 2 | romaric.jpg |
- and, finally, a
article_with_video
table:
id | article_id | videoPath |
1 | 3 | video.mp4 |
The schema is way more complex, but Doctrine will gracefully handle it and build entities from multiple tables. It implies a lot of JOIN
operations, with eventually some impact on performance and query complexity (ie., writing manual queries becomes harder).
A limited use, and what you really want to do
From a technical point of view, STI and CTI both brings some complexity, along some technical limitations. For instance, lazy-loading won’t be available in some scenarii.
But the biggest issue for me is that most of the time, having entity inheritance does not make any sense. My previous example probably looked bad to some of you, and you are right. Composition, ie. having 3 separate Article
, Picture
and Video
entities, would have been a better choice.
On real-world cases, I remember seeing only once entity polymorphism being justified. At that time, I implemented CTI, but later technical limitations, especially related to querying, slowed down the project.
Hence, my piece of advice: avoid STI and CTI.
Mapped Superclass
There is another type reference in Doctrine documentation I skipped: Mapped Superclass. That one is more of a mapping trick, because it won’t impact database schema. The “mapped superclass”, the parent class, is not an entity. It merely provides some properties to children, which will be stored in every child table.
The perfect example is the User
class from FOSUserBundle. Typically, your project User
entity class will extend it, it provides some base fields (email
, password
…).
Outside of that very specific scenario, ie. of a bundle providing a “base” entity, I don’t see much use to it.
Final word
I hope the different mode of Doctrine inheritance mapping, and their limitations, are clearer now. Most of the time, I think we want to achieve code reuse, or de-duplication, instead of real polymorphism. PHP has a built-in mechanism for that: traits. Doctrine ORM supports those out of the box (for scalar values, ie., not associations), without any additional configuration. So, as a final word, you may want to consider using traits next time you want to share some properties across entities.
Comments