The Problem
Doctrine ORM claims to support some kind of inheritance but I have yet to see a good example. The inheritance documentation on the Doctrine ORM site could be worse but it certainly has a lot of room for improvement. How about an example with some actual data? The symfony documentation for Doctrine inheritance lays things out a little more clearly than the Doctrine docs but it still wants for good examples.
The Solution
Here I intend to perform a hearty exploration of Doctrine inheritance with the hope that I can show, by example, what it can and can’t do. The inheritance model I will use will be simple: the base class will be called FarmAnimal and the three inheriting classes will be Cow, Dog and Chicken. All FarmAnimals will have at least a name, a sound and a number of legs. Each type of FarmAnimal will have the following unique properties: Cow will have a use (beef or dairy), a Dog will have a breed and a Chicken will have an egg color.
If you’d like to follow along, I’ve put together some setup instructions for this project.
Doctrine offers three styles of inheritance: Simple, Concrete and Column Aggregation. The symfony docs give the following definitions:
| Name | Description |
|---|---|
| Concrete | Each child class has a separate table has all the columns of its parents |
| Simple | Each child class shares the same table and columns as its parents |
| Column Aggregation | All columns must be defined in the parent and each child class is determined by a type column |
Simple Inheritance
The Doctrine documentation shows an example where we have a User class and a Group class. Both of these classes inherit from Entity and neither one has any unique properties. When the example schema is evaluated, Doctrine only spits out one table: entity. If that’s all that “simple inheritance” is good for, I don’t really see how that’s inheritance. Any object you might instantiate, whether it be a User, Group or plain Entity, is an Entity, nothing more and nothing less. What’s gained there?
Let’s try something similar to their example but go a little further with it.
config/doctrine/schema.yml
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: simple
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: simple
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: simple
columns:
egg_color:
type: enum
values: [brown, white]Since we’re using enums, we’ll have to update config/databases.yml and add the use_native_enum directive:
all:
doctrine:
class: sfDoctrineDatabase
param:
dsn: 'mysql:host=localhost;dbname=inheritance'
username: inheritance
password: pass123
attributes:
use_native_enum: trueNow let’s build the model, build the SQL and insert the SQL:
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
What does this give us in the database?
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | farm_animal | +-----------------------+ 1 row IN SET (0.00 sec) mysql> DESCRIBE farm_animal; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | | breed | varchar(20) | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 9 rows IN SET (0.01 sec)
As you can clearly see, all the inheriting classes’ properties have been rolled into one table along with the base class’s properties. This is exactly what the Doctrine doc tells us, but it doesn’t totally spell it out with an example.
The table above is a bad model, both from a purist and pragmatic perspective. A purist might say, “We have three distinctly different types of things but only one data structure! Are we expected to deduce, based on which values are NULL and which are not, which type of FarmAnimal we’re dealing with? What if we add the idea of a Cat with a breed? How do we tell it apart from a Dog?” And a pragmatist might say, “What if we have 20 different types of FarmAnimals, each having 5 different properties? That’s at least 100 columns in the FarmAnimal table!” I would agree with both the pragmatist and the purist. Imagine looking at a table with 100 columns, most of the rows having NULL in most of the columns most of the time. That’s an extreme example, but the principle is the same on any scale.
If you look at the PHP classes created by symfony, they’re meaningless. Take a look at lib/model/doctrine/base/BaseChicken.class.php, for example:
/** * BaseChicken * * This class has been auto-generated by the Doctrine ORM Framework * * * @package INHERITANCE * @subpackage model * @author Your name here * @version SVN: $Id: Builder.php 6820 2009-11-30 17:27:49Z jwage $ */ abstract class BaseChicken extends FarmAnimal { public function setUp() { parent::setUp(); } }
There’s nothing unique to Chicken there. All the properties of all the FarmAnimals are kept in lib/model/doctrine/base/BaseFarmAnimal.class.php:
abstract class BaseFarmAnimal extends sfDoctrineRecord { public function setTableDefinition() { $this->setTableName('farm_animal'); $this->hasColumn('name', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('sound', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('leg_count', 'integer', null, array( 'type' => 'integer', )); $this->hasColumn('created_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('updated_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('purpose', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'beef', 1 => 'dairy', ), )); $this->hasColumn('breed', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('egg_color', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'brown', 1 => 'white', ), )); } public function setUp() { parent::setUp(); } }
I’ve seen enough of “simple inheritance” to know that I would never use it. Let’s see what “concrete inheritance” is all about.
Concrete Inheritance
Apparently all that’s needed to switch from simple inheritance to concrete inheritance is to replace all instances of type: simple with type: concrete:
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: concrete
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: concrete
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: concrete
columns:
egg_color:
type: enum
values: [brown, white]Let’s run our symfony commands again:
$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
What’s in the database now?
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | chicken | | cow | | dog | | farm_animal | +-----------------------+ 4 rows IN SET (0.00 sec)
That’s encouraging: each class has its own separate table.
mysql> DESCRIBE farm_animal; +------------+-------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+-------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | +------------+-------------+------+-----+---------+----------------+ 6 rows IN SET (0.00 sec) mysql> DESCRIBE cow; +------------+----------------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+----------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | +------------+----------------------+------+-----+---------+----------------+ 7 rows IN SET (0.00 sec) mysql> DESCRIBE dog; +------------+-------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+-------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | breed | varchar(20) | YES | | NULL | | +------------+-------------+------+-----+---------+----------------+ 7 rows IN SET (0.01 sec) mysql> DESCRIBE chicken; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 7 rows IN SET (0.00 sec)
That looks like what I want. Each entity is represented by a table that has a column for each of its properties, nothing more and nothing less. The only way this might be improved, in my opinion, is if we could tell Doctrine that FarmAnimal is an abstract class and we don’t want a table for it.
If we look at the symfony-generated classes, they look how we’d expect:
abstract class BaseChicken extends FarmAnimal { public function setTableDefinition() { parent::setTableDefinition(); $this->setTableName('chicken'); $this->hasColumn('egg_color', 'enum', null, array( 'type' => 'enum', 'values' => array( 0 => 'brown', 1 => 'white', ), )); } public function setUp() { parent::setUp(); } }
abstract class BaseFarmAnimal extends sfDoctrineRecord { public function setTableDefinition() { $this->setTableName('farm_animal'); $this->hasColumn('name', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('sound', 'string', 20, array( 'type' => 'string', 'length' => '20', )); $this->hasColumn('leg_count', 'integer', null, array( 'type' => 'integer', )); $this->hasColumn('created_at', 'timestamp', null, array( 'type' => 'timestamp', )); $this->hasColumn('updated_at', 'timestamp', null, array( 'type' => 'timestamp', )); } public function setUp() { parent::setUp(); } }
Doctrine’s “concrete inheritance” looks pretty solid. Let’s take a look at the third and final style, “column aggregation.”
Column Aggregation
To be honest, I’m pretty baffled as to what this one is supposed to do for us. Hopefully this example will make things clearer.
FarmAnimal:
columns:
name: string(20)
sound: string(20)
leg_count: integer
created_at: timestamp
updated_at: timestamp
Cow:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 1
columns:
purpose:
type: enum
values: [beef, dairy]
Dog:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 2
columns:
breed: string(20)
Chicken:
inheritance:
extends: FarmAnimal
type: column_aggregation
keyField: type
keyValue: 3
columns:
egg_color:
type: enum
values: [brown, white]$ symfony doctrine:build-model $ symfony doctrine:build-sql $ symfony doctrine:insert-sql
mysql> SHOW TABLES; +-----------------------+ | Tables_in_inheritance | +-----------------------+ | farm_animal | +-----------------------+ 1 row IN SET (0.00 sec) mysql> DESCRIBE farm_animal; +------------+-----------------------+------+-----+---------+----------------+ | FIELD | Type | NULL | KEY | DEFAULT | Extra | +------------+-----------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | AUTO_INCREMENT | | name | varchar(20) | YES | | NULL | | | sound | varchar(20) | YES | | NULL | | | leg_count | bigint(20) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | type | varchar(255) | YES | | NULL | | | purpose | enum('beef','dairy') | YES | | NULL | | | breed | varchar(20) | YES | | NULL | | | egg_color | enum('brown','white') | YES | | NULL | | +------------+-----------------------+------+-----+---------+----------------+ 10 rows IN SET (0.01 sec)
One table again, just like simple inheritance. Since I already rejected this structure, I see no reason to continue with column aggregation.
Conclusion
In my opinion, Doctrine’s simple inheritance and column aggregation are invalid and concrete is the only way to go. I hope these examples cleared up some confusion for anyone who had as much trouble with these concepts as I did.
Concrete inheritance doesn’t work well with relations between models because of strict foreign keys checks (Mysql/Innodb). So if you have such schema:
—
detect_relations: true
User:
columns:
username: string
Admin:
inheritance:
extends: User
type: concrete
Moderator:
inheritance:
extends: User
type: concrete
Document
columns:
description: string
user_id: integer
then it will be not possible to save a Document, say, for an Administrator, because mysql will try to check if such user exists in User(!) table, not in administrator table. So it why i’m forced to use column aggregation inheritance.
Nini, if there’s a one-to-one relationship between User and Admin, what’s wrong with having Document’s foreign key point to User?
Good post!
I agree with the conclusion that concrete inheritance mode is the best one among the three. But the mileage may vary depending on the specific need and circumstances.
I wish to add the issue with concrete inheritance which nini has just raised. Even the concrete inheritance mode is not complete enough for the needs of true object-oriented logic: although there exists a top-level table farm_animal, this table will always be empty. The dogs, cows and chicken are exclusively stored in their respective tables, and none of them at all in the base class table. This arrangement is unlucky, as it inhibits browsing and quering through _all_ FarmAnimals, no matter which their (sub-)class is.
So e.g. it is not possible to search for an animal called “Rosa”, without knowing whether Rosa is a dog or a cow.
And more importantly, as nini is stating, you can’t setup an association between FarmAnimal and e.g. Farm (“a farm has zero, one or more animals”).
Doctrine 2.0 may sometimes offer true full inheritance functionality by introducing “table inheritance”. That mode should avoid any redundancy (no same field exists in multiple tables) and then have the base class’ table store its part of each object, now allowing queries to be made on FarmAnimal level.
The key advantage I found with using the column aggregation is that you can use dql to query the parent table and then get back the child objects. This will hopefully come in real handy on a project I’m working on where there’s a public timeline of actions and when I query get the last 20 actions I only need to query one table and every entry in the timeline can have it’s own individual ID
Agree with Kev.
Concrete Inheritance is used primarily for different entities of the same parent class
example 1: types of ‘people’: user, agent, admin, supplier contact, customer contact, employee
example 2: options (name, value): gender, phone-type, account types, dept codes
What I’d like to find is a method of having the objects so that the common values are in the parent table and only the sub-class specific columns are in a separate table (that is handled auto-magically, as that is what I am doing by hand currently).
cow (purpose) -> farm_animal (name, sound, leg_count)
milk_cow (quantity) -> cow (purpose) -> farm_animal