Inheritable Behavior - Missing link of Cake Model

By taylor luk (taylor.luk)
Object relational mapping is never trivial, in order to to map a relational database to object model. there are multiple area involved, Inheritable Behavior as its name may suggested. It is the missing link in current CakePHP ORM implementation.

Consider this behavior to change, develop test cases before you roll it into your app. :)
This is my third attempt to implement model inheritance in CakePHP. first I tried with a behavior and failed miserably, second time around, hacking into AppModel and end up with a very ugly hack.

After i saw subclass behavior http://bakery.cakephp.org/articles/view/subclass-behavior, and ExtendableBehavior http://bakery.cakephp.org/articles/view/extendablebehavior last night. And surprisingly CakePHP core is much stablized and i decide to give it a try and based on the work they have done, credits to them


Goal

  1. Clean api and minimal configuration
  2. Implement Single-table-inheritance
  3. Implement class-table-inheritance
  4. First time trying test-driven when doing CakePHP behavior
  5. Fix some design issue in other two behaviors

Background

some background on this topic, and quoting my own ticket comment and some discussion from rails and django camp
  1. A long outstanding enhancement request at CakePHP trac https://trac.cakephp.org/ticket/1365
  2. Explaination of single table inheritance http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
  3. Martin fowler's explaination on class table inheritance http://www.martinfowler.com/eaaCatalog/classTableInheritance.html
  4. http://code.djangoproject.com/wiki/ModelInheritance
  5. http://wiki.rubyonrails.org/rails/pages/Inheritance


Note

  1. Haven't implement deep inheritance, ie: Dog < Pet < Mammal don't be surprise if it fails
  2. Associated inheritable model doesn't work since CakePHP ignore behavior callbacks beforeFind/afterFind https://trac.cakephp.org/ticket/2056
  3. Single-Table inheritance is quite heavily tested with association and behaviors
  4. Class-table inheritance used some dirty approach with some smarts, feel free to improve implmentation and test cases
  5. Beware performance implications and use with care

The code

Behavior and test cases are included, just extract to your cakephp root directory

https://trac.cakephp.org/attachment/ticket/1365/inheritable_behavior.zip?format=raw

Examples


Single Table inheritance

Represents an inheritance hierarchy of classes as a single table that has columns for all the fields of the various classes.
table people
FieldType
idint
full_namestring
typestring
project_idint


this is a classically example of person, employee and employer class model, and when loading the behavior it uses STI method by default.

Model Class:

Download code <?php 
class Person extends AppModel {
    var 
$useTable 'people';
    var 
$belongsTo = array('Project');
    var 
$hasMany => array('Task')
}
?>

Model Class:

Download code <?php 
App
::import('Model''Person');
class 
Employee extends Person {
    var 
$actsAs = array('Inheritable')
}
?>

Model Class:

Download code <?php 
App
::import('Model''Person');
class 
Manager extends Person {
    var 
$actsAs = array('Inheritable')
}
?>

Class Table inheritance

Represents an inheritance hierarchy of classes with one table for each class.
table assets
FieldType
idint
titlestring
descriptionstring
createdint
modifiedint

table images
FieldType
idint
content_typestring
file_namestring
file_sizestring

table documents
FieldType
idint
file_namestring
file_pathstring
thumbnailstring

table links
FieldType
idint
urlstring

When you do a find on a concrete subclass it will join with parent model and merge the result together


when you save, it will find out which field belongs to parent model's schema and save it there.

class-table inheritance is usually hard to implement, currently it doesn't cover all the edge cases such as behavior loading, rewriting find conditions.



Model Class:

Download code <?php 
class Asset extends AppModel {
   var 
$name 'Asset';

}
?>

Model Class:

Download code <?php 
App
::import('Model''Asset');
class 
Image extends Asset {
    var 
$actsAs = array(
        
'Inheritable'=> array('method'=>'CTI')
    );
}
?>

Model Class:

Download code <?php 
App
::import('Model''Asset');
class 
Document extends Asset {
    var 
$actsAs = array(
       
'Inheritable'=> array('method'=>'CTI')
    );
}
?>

Model Class:

Download code <?php 
App
::import('Model''Asset');
class 
Link extends Asset {
    var 
$actsAs = array(
       
'Inheritable'=> array('method'=>'CTI')
    );
}
?>

 

Comments 807

CakePHP Team Comments Author Comments
 

Comment

1 thoughts anyone?

Well, hundreds of views and no comments.

Its true that this behavior is only useful for some use cases when complex application needs inheritance base model. Employee, Employer, Person is a classic example.

In application that requires simple table inheritance this could work really well.

Let me know if you have questions or problems getting this code to work.

Cheers :)
Posted Nov 13, 2008 by taylor luk
 

Bug

2 Problem

I have a problem with this behavior.

My model (simplified) is this one:

class Proyecto extends AppModel {
$belongsTo=array('User' => array('className' => 'User','foreignKey' => 'user_id','conditions' => '','fields' => '','order' => '');
}

class Potencia extends Proyecto {
var $name = 'Potencia';
var $validate = array(
'proyecto_id' => array('numeric')
);

var $actsAs = array(
'Inheritable'=> array('method'=>'CTI') /*Significa: Utilitza el Behavior amb em metode Class Table Inheritance (CTI)*/
);
}

When I find over Porencia, I have an SQL error (SQL Error: 1054: Unknown column 'Armonico.user_id' in 'on clause').
I think cake searches the users as if the subclass where directly linked with the users model.
Let me know if I am wrong or it is a bug.

Thank you!
Posted Dec 16, 2008 by Marc Bernet
 

Comment

3 Type for CTI

I really like this approach but how come CTI does not use a type field? What if I wanted to list all Assets and sort them by type?
Posted Dec 21, 2008 by Steve Oliveira
 

Comment

4 Storing type for CTI and deeper inheritance

I've modified the behavior to allow for a "type" field in CTI

Go to saveParentModel method and change $parentData to the following:

<?php
$parentData 
= array(
   
$model->parent->primaryKey => $model->id,
   
$model->inheritanceField => $model->alias
);?>

You'll also need to create the type field in the table of your superclass.

Also, if you have deeper inheritance there will be a problem with setting values to the main class. Here's an example:

Person > Users > Administrators

Let's say "Person" has the field "first_name". If you want to set that field through the Administrator class, it won't work. In order to make it work you have to go to the saveParentModel and remove the if statement
if (in_array($key, $fields)) {
This prevents any field that is not in the immediate parent class from being included. $fields is the list of fields from $model->parent->schema() ... and in this case, if we're adding an administrator, the parent model is User. The schema() method, therefore, does not include the fields from Person, it only includes the fields from User. So if you have the data 'Administrator.first_name' it won't be added because "first_name" is a field from the Person model.

By removing the if statement, you're getting rid of the restrictions on what fields can be passed. So the foreach loop should look like this

In the foreach loop it should look like this:

<?php
foreach ($model->data[$model->alias] as $key => $value) {
   
$parentData[$key] = $value;
}
?>

If we can some how get $model->parent->schema() to get all the fields, even those included in that model's parents... then this wouldn't be required, but at the moment I don't see how that can be done.

Posted Dec 21, 2008 by Steve Oliveira
 

Comment

5 RE:Type for CTI

I really like this approach but how come CTI does not use a type field? What if I wanted to list all Assets and sort them by type? Hi Steve Oliveira thanks for sharing as such myself and many may find useful.

1. Sorry for not including "type" field in the schema table, it always requires "type" field for CTI to work.

2. thanks for the bug fix and the saveParentModel method didn't include $model->inheritanceField is a bug

3. deep inheritance is on my todo list, i haven't got time to investigate further, can you confirm if deep inheritance saving works after such modification? as well as how does deep inheritance behaves for the whole CRUD operation.

Thanks and looking forward for you feedback
Posted Dec 28, 2008 by taylor luk
 

Comment

6 thanks

Thanks for this post. It has brought the issue of "object-relational impedance mismatch" to my knowledge. This has been troubling me for some time now, so it's good to be able to put a name to it! So many thanks.
Posted Dec 29, 2008 by Phil McClure
 

Comment

7 Deeper Inheritance

can you confirm if deep inheritance saving works after such modification? as well as how does deep inheritance behaves for the whole CRUD operation.
Create/Update/Delete works, but not flawlessly.

Here's the example I'm working with.
Node > Contacts > User

User is a contact; contact is a node.

When I save a user the callback methods for all models are executed, but if I define a callback method in user (ex: beforeValidates()), ideally the parent functions should only be called if I explicitly call parent::beforeValidate(). I guess because they're callbacks they will be called for all models involved in saving/retrieving data, but it doesn't seem like expected behaviour since I am really just saving a User and if User::beforeValidate() doesn't call parent::beforeValidate() I don't think it should be executed.

Deeper inheritance doesn't seem to work with reading data. In my Node > Contact > User example, when I read a User record only the Contact fields are included. Node is not included in the association nor are the fields in the results array.

Hope that helps
Posted Jan 5, 2009 by Steve Oliveira
 

Bug

8 My bad...

Originally said in this comment that it didn't work well with TreeBehavior, but it does work well with it.
Posted Jan 7, 2009 by Steve Oliveira
 

Comment

9 Won't update parent model

For some reason the afterSave method of the behavior is set to only allow saving of the parent model if a new record is being created.

Here is the code:

if ($created && $method == 'CTI') {
   return $this->saveParentModel($model);
}

I'm not sure why this was initially set like this, but if I update the child model, I would also want the parent model fields to reflect the changes. So in order to "fix" this, get rid of the $created clause in the if statement.
Posted Jan 7, 2009 by Steve Oliveira
 

Comment

10 Possible workaround for parent associations

Say Digit extends Widget and Widget belongs to Doodles.

First off, I receive an join error because doodle_id does not exist on Digit (but it does on Widget), so I overwrote the $belongsTo in Digit with:

Model Class:

<?php 
class Digit extends Widget {
   var 
$name 'Digit';
   var 
$actsAs = array(
       
'Inheritable' => array('method' => 'CTI')
   );
   var 
$belongsTo = array();
}
?>

In order to still deep fetch the Doodles, I hard-coded recursive to 2 in the classTableBeforeFind method (Note I tried setting the recursive before the find but it was wiped out).

The method should look something like this after:


extract($this->settings[$model->alias]);
$bind = array('belongsTo' => array(
   "{$model->parent->alias}" => array(
       'className' => $model->parent->alias, 'foreignKey' => "{$model->primaryKey}"
   )
));
$model->bindModel($bind);
$query['recursive'] = 2;


Now this might be complete crap, but it works. Does this spark any ideas for a better implementation?
Posted Feb 16, 2009 by Rob
 

Comment

11 How dos CTI work in here?

I mean, there's many ways of storing a class hierarchy in a relational database using different tables. Even the article you provided doesn't clear out how, and you don't specify what those id really are.

So I don't really understand wich way you chose, so first, I'm gonna say you should add some examples using find and save and what do they return.

One way I need to, is that the superclass doesn't actually have an associated table. I can go around that, but still... seems imposible like this.
Also, the most common way I see and used this in object oriented languages is, the superclass has a table wichs primary key is generated autoincremental/serial, then subclasses primary key is in fact a foreign key of the superclass. You didn't put much into the tables you posted, so I can't know how you intended it to work, but certainly is not this way. Or if it is, it's just not working how it should.
You can have an independent primary key in each table, but then you're gonna have to use a foreign key to the superclass, and that would make the database not normalized, since it depends on your primary key.

So, how? Wich way is it? I read someone added a 'type' field somewhere, wich is not needed and probably shouldn't be used.

What I also see unexplained is the type field in STI... is it mandatory, it has to be named 'type' and needs to be string?

Props for the initiative, I hope this goes far, but as much as this is a decent aproach, I believe it should be done in AppModel, well done, by core CakePHP developers too.
Posted Mar 23, 2009 by Franco Bonazza
 

Comment

12 few questions...

I have had trouble implementing this behavior (CTI method) for models that have associations. My example is:

Model Class:

<?php 
class Asset extends AppModel {
    var 
$belongsTo = array('User');
}
?>


Model Class:

<?php 
class Image extends Asset {
    var 
$actsAs = array('Inheritable' => array('method' => 'CTI'));
}
?>


When I try:

Controller Class:

<?php 
$images 
$this->Image->find('all');
?>

I get an error because Cake is looking for the user_id field in my Image table, however I want this field to live in the Assets table since all assets will share this property.

Any tips?
Posted Apr 12, 2009 by zach
 

Comment

13 Override CTI

@zach: in the base class (Asset) you have a belongsTo variable. You need to set this to an empty array in the classes that extend the base class (Asset).

In other words the Image class should have the line:
var belongsTo = array();
Posted Apr 13, 2009 by Rob
 

Comment

14 Thanks Rob

Thanks Rob!
Posted Apr 13, 2009 by zach
 

Comment

15 Thanks Rob

Thank you Rob, that got rid of my error!

My next question is about the implementation, specifically, as Franco mentioned above, most CTI implementations give a primary key to the super class and then save that same key as a foreign key in the subclass, for example


assets table
-----
id: 1
title: my fist picture
-----
id: 2
title: my link
-----
id: 3
title: my second picture
-----


images table
-----
id: 1
filename: file1.jpg
-----
id: 3
filename: file2.jpg
-----



links table
-----
id: 2
url: http://mypage.com
-----

I assumed that I would set only the assets table's id field to auto increment and the images and links table's ids would be set to the asset table's primary key automatically. Thus the images and assets tables would not have an auto increment field.

I have also tried adding a field 'asset_id' to the subclasses' tables, but the foreign key to the asset table is not being set.

How can I store data for more than one subclass if the superclass' table doesn't have the primary key for all subclasses and there is no foreign key within the subclasses?

Is this something that I need to handle manually or am I missing something?
Posted Apr 13, 2009 by zach
 

Comment

16 type field needed when using cti

@zach you need a varchar field field named 'type' in the base table. This way your base class knows which row belongs to which subclass.
Posted Apr 14, 2009 by Rob
 

Comment

17 got that... but what about the ids?

Hi Rob,

I have the 'type' field and it is working perfectly... What I am looking for is an answer as to how the superclass record is joined with the correct subclass record.

It seems that the implementation should be saving the primary id of the superclass record (the Assets.id field in my example) in the related subclass record (Image.id or Image.asset_id).

Otherwise the Asset.title and Image.filename records for a particular Image will have no way to be matched up.

My initial thinking was that the Image.id field would be set automatically to match its related Asset.id. This means I would set only the Asset.id field to auto increment, with its subclasses ids being set automatically by the behavior to the same value...

I don't see this happening automatically, so I am wondering if I missed something or if I need to handle the subclass id setting on my own?
Posted Apr 14, 2009 by zach
 

Question

18 Can't get inheritance to work

I'm pretty much a beginner when it comes to CakePHP, and I can't get the subclass behavior to work.

My simplified models look like this:

Model Class:

<?php 
class Person extends AppModel{
    var 
$name 'Person'
}
?>

Model Class:

<?php 
App
::import('Model''Person');
class 
Child extends Person{
    var 
$actsAs = array(
        
'Inheritable' => array(
            
'method' => 'CTI'
        
)
    );
}
?>

When i do find('all'), the system gives me 'undefined index' notices on everything from the subclass table, and the query looks like this:
SELECT Child.id, Child.info, Child.created, Child.modified
FROM persons AS Child
Basically, it doesn't join with the Child table. What am I doing wrong?

Edit: typo
Posted Apr 23, 2009 by Petter Brodin
 

Comment

19 Extending CTI

I think even though you named the class "Supported", you still have to extend the actual class Person.
Posted Apr 23, 2009 by Rob
 

Comment

20 My bad

There was some copy-paste problem there, and I displayed the wrong code here in the post. Now it's correct, and displays as it is used in the application, but the problem still exists.
Posted Apr 23, 2009 by Petter Brodin
 

Question

21 Where should I place the extracted files

Some experimenting with deleting the Inheritable Behavior files makes me think that I might have placed the extracted files in the wrong folder. As the article tells me to, I downloaded the rar-file and extracted it to the root of my cakePHP installation, so that it now contains four folders: app, cake, vendors and inheritable_behavior. Is this right?
Posted Apr 27, 2009 by Petter Brodin