Using doctrine with symfony 1.2 (2/4) - inheritance

Publié le par greg

Hello, this is time for the second article on Doctrine with symfony about inheritance this time. As I am used to Postgresql's inheritance mechanism, I was curious as Doctrine proposes 3 ways to create inheritance with your objects model :
  • simple inheritance
  • concrete inheritance
  • column aggregation inheritance
Let's (again) take an example: we want to create an application for a show that rents bikes, motorcycles and maybe more. We can say, our shop rents vehicules. All of the vehicules can be one of bike, motorcycle, car or whatever. We can easily understand a car is different from a bike and they do not have the same caracteristics.

The first step is to create 2 tables, one vehicle and one bicycle table:
config/doctrine/schema.yml:
 Vehicle:
tableName: vehicle
actAs: [Timestampable]
columns:
ref: { type: char(6), primary: true }
brand: { type: varchar(255), notnull: true }
Bicycle:
inheritance: { type: simple, extends: Vehicle }
columns:
bike_type: { type: enum, values: [city, mountain, race], default: city, notnull: true }
gears: { type: integer(32), default: 1, notnull: true }
seats: { type: integer(4), default: 1, notnull: true }
checks:
gears_min: gears > 0
seats_min: seats >


We see here, we have defined a simple inheritance. So what did Doctrine ?
data/sql/schema.sql:
 CREATE TABLE vehicle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
bike_type VARCHAR(255) DEFAULT 'city' NOT NULL,
gears BIGINT DEFAULT 1 NOT NULL,
seats INT DEFAULT 1 NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref)
);
(Our check statements do no appear) It sounds rather limited but let's see what we can do with that, assuming we add 2 bikes in the database using fixtures:
 ./symfony doctrine:dql "SELECT * FROM Bicycle"
>> doctrine executing dql query
DQL: SELECT * FROM Bicycle
found 2 results
-
ref: BK0478
brand: peugeot
bike_type: city
gears: '4'
seats: 1
created_at: '2009-01-04 17:18:25'
updated_at: '2009-01-04 17:18:25'
-
ref: BK0734
brand: raleigh
bike_type: mountain
gears: '21'
seats: 1
created_at: '2009-01-04 17:18:25'
updated_at: '2009-01-04 17:18:25

This isn't really an inheritance mechanism because the children doesn't inherit from its parent. It is more like the parent get richer with every child classes. This may work with very simple cases because in this application, the motorcycle may have columns with «not null» attributes that bikes will never comply with. This isn't really what we are looking for.

Let's change our schema to use the concrete inhéritance :
config/doctrine/schema.yml:
 Bicycle:
inheritance: { type: concrete, extends: Vehicle }
Launche the build-all-reload and see what we have:
data/sql/schema.sql:
 CREATE TABLE vehicle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref)
);

CREATE TABLE bicycle (
ref CHAR(6),
brand VARCHAR(255) NOT NULL,
bike_type VARCHAR(255) DEFAULT 'city' NOT NULL,
gears BIGINT DEFAULT 1 NOT NULL,
seats INT DEFAULT 1 NOT NULL,
created_at TIMESTAMP without time zone,
updated_at TIMESTAMP without time zone,
PRIMARY KEY(ref),
CHECK (gears > 0),
CHECK (seats > 0)
);

It sounds like Doctrine has created two tables and wants to handle the inheritance by itself. Let's see what we have once the fixtures are loaded:
In our database:
 dbtest=> \d
List of relations
Schema | Name | Type | Owner
--------+---------+-------+-------
public | bicycle | table | toto
public | vehicle | table | toto
(2 rows)

dbtest=> SELECT * FROM vehicle ;
ref | brand | created_at | updated_at
-----+-------+------------+------------
(0 rows)



dbtest=> SELECT * FROM bicycle ;
ref | brand | bike_type | gears | seats | created_at | updated_at
--------+---------+-----------+-------+-------+---------------------+---------------------
BK0478 | peugeot | city | 4 | 1 | 2009-01-04 17:25:26 | 2009-01-04 17:25:26
BK0734 | raleigh | mountain | 21 | 1 | 2009-01-04 17:25:26 | 2009-01-04 17:25:26
(2 rows)



We can see we have 2 bikes but it also appears we have no vehicules ! So Doctrine does not handle the inheritance by itself, if we want to have 2 bikes and also say we have 2 vehicles we have to overload the Bicycle definition. The other solution is to declare the Vehicle as an abstract class:
config/doctrine/schema.yml:
 Vehicle:
tableName: vehicle
abstract: true

But this seems not to work (or I missunderstood the doc) because the table definition is still present in the SQL...

Anyway, I am now able to create a motorbike table adding the following to
config/doctrine/schema.yml:
 Motorcycle:
inheritance: { type: concrete, extends: Vehicle }
columns:
type: { type: enum, values: [scooter, roadster, city], notnull: true, default: scooter }
cylinder: { type: integer, notnull: true }
plate: { type: string(11), notnull: true, unique: true }
checks:
cylinder_min: cylinder > 0
plate_format: plate ~ \'[0-9]{1,4}\\-[A-Z]{2,3}\\-[0-9]{1,2}\

So after adding some fixtures I have 2 tables with independant data:
 dbtest=> SELECT * FROM motorcycle ;
ref | brand | type | cylinder | plate | created_at | updated_at
--------+--------+---------+----------+------------+---------------------+---------------------
MB1837 | vespa | scooter | 125 | 324-MA-92 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
MB6430 | yamaha | city | 750 | 2301-ZC-60 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
(2 rows)



dbtest=> SELECT * FROM bicycle ;

ref | brand | bike_type | gears | seats | created_at | updated_at
--------+---------+-----------+-------+-------+---------------------+---------------------
BK0478 | peugeot | city | 4 | 1 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
BK0734 | raleigh | mountain | 21 | 1 | 2009-01-08 19:37:10 | 2009-01-08 19:37:10
(2 rows)



dbtest=> SELECT * FROM vehicle ;
ref | brand | created_at | updated_at
-----+-------+------------+------------
(0 rows)



If I try to count the vehicles I have with a DQL query it would not be as simple as:
 test$ ./symfony doctrine:dql "SELECT * FROM Vehicle"
>> doctrine executing dql query
DQL: SELECT * FROM Vehicle
>> doctrine no results found

Let's then try the last type of inheritance: the columns aggregation.

 Bicycle:
inheritance: { type: column_aggregation, extends: Vehicle, keyField: type, keyValue: bicycle }
For the bicycles and
 Motorcycle:
inheritance: { type: column_aggregation, extends: Vehicle, keyField: type, keyValue: motorbike }
for the motorbikes.

Then doctrine complains it cannot load the data because constraints. If I go and have a check at the database I see there is only one table with all the columns but with an extra column named type and none of my sql constraints:
 dbtest=> SELECT * FROM vehicle ;
ref | brand | type | bike_type | gears | seats | cylinder | plate | created_at | updated_at
-----+-------+------+-----------+-------+-------+----------+-------+------------+------------
(0 rows)
At the end of this testing phase I am a bit confused about what these types of inheritance stand for. I am not sure I will ever use one of those. I was expecting Doctrine to have like a PHP implementation of what Postgresql's inheritance system is without the problems it has ... or maybe I am missing something :o)

Publicité

Publié dans symfony

Pour être informé des derniers articles, inscrivez vous :
Commenter cet article
F
<br /> As we say in French, "mieux vaut tard que jamais". Propel implements something that looks like your ideal model inheritance:<br /> http://propel.posterous.com/new-in-propel-15-concrete-table-inheritance-a.<br /> <br /> <br />
Répondre
G
Thanks for your comments.@brett: ending with lot of tables with 1-n relationship around (let's say) a «type» column this is where we all are (in 2009 this is sad). But this implies lot of programming in each project to turn that into inheritance from an ORM point of view and this is also more complex to maintain.@Francois: In fact I ended my article thinking none of the inheritance types proposed here seem to help. The inheritance system I found the best is the Postgresql's one but because there are problems with foreign keys on ancestor tables I do not use it either. We have to wait :o)@Eric : ok, I will spend more time on column aggregation inheritance and complete my article if I found something new.
Répondre
É
That's a great article.But that's odd that you didn't succeed to use the column_aggregation inheritance, it works pretty well on my current symfony project.Here the part of my schema.yml where I use it :<br /> Document:<br />   options:            { collate: utf8_unicode_ci, charset: utf8 }<br />   actAs: [Timestampable]<br />   columns:<br />     project_id:         { type: integer,              notnull: true }<br />     name:               { type: varchar, length: 100 }<br />     identifiant:        { type: varchar, length: 50 }<br />   relations:<br />     Project:            { onDelete: CASCADE }<br /> <br /> Estimate:<br />   inheritance:<br />     extends: Document<br />     type: column_aggregation<br />     keyField: type<br />     keyValue: estimate<br />   columns:<br />     state:               { type: enum,    values: [draft, validated, rejected], notnull: true }<br /> <br /> Amendment:<br />   inheritance:<br />     extends: Document<br />     type: column_aggregation<br />     keyField: type<br />     keyValue: amendment<br />   columns:<br />     state:               { type: enum,    values: [draft, validated, rejected], notnull: true }<br /> <br /> Invoice:<br />   inheritance:<br />     extends: Document<br />     type: column_aggregation<br />     keyField: type<br />     keyValue: invoice<br />   columns:<br />     state:              { type: enum,    values: [draft, sent, payed], notnull: true }<br />     payment:            { type: enum,    values: [total, partial] } 
Répondre
F
Excellent article. It seems that concrete inheritance should be extended to make a copy of each child record in the parent table(s), and keep a 1-1 relationship between parent-child. Also, what happens if I want to create a 'child_bicycle' table inheriting from 'bicycle' ? Neither of the inheritance types described here seem to help.
Répondre
B
<br /> <br /> Thanks for the article. It helps me understand how Doctrine currently handles inheritance without doing the experiments myself. The first database I learned was PostgreSQL. What a great piece of software. The first database schema that I developed used  inheritance and they were very easy to use. The problem for an ORM like Doctrine is that each DB maker implements inheritance differently or not at all. PostgreSQL inheritance rocks. Oracle inheritance is okay, but weird and verbose. And MySQL doesn't handle inheritance IIRC. It must be darn difficult to program for all of the variations that exist between the various databases.I switched to MySQL mainly because nearly all open source projects are programmed for MySQL first and other databases second or not at all. Because of this I switched to using  tables with n:1 identifying relationships. This allows for a central table that has common data with many tables that are specific to their data.
Répondre