Inheritance with symfony and Doctrine ORM

 

From: http://jasonswett.net/blog/inheritance-with-symfony-and-doctrine-orm/


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:
NameDescription
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: true
Now 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.
posted @ 2012-03-22 18:37  Lux.Y  阅读(...)  评论(...编辑  收藏