Inheritable Behavior - Missing link of Cake Model
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. :)
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
https://trac.cakephp.org/attachment/ticket/1365/inheritable_behavior.zip?format=raw
table people
this is a classically example of person, employee and employer class model, and when loading the behavior it uses STI method by default.
table assets
table images
table documents
table links
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.
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
- Clean api and minimal configuration
- Implement Single-table-inheritance
- Implement class-table-inheritance
- First time trying test-driven when doing CakePHP behavior
- 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- A long outstanding enhancement request at CakePHP trac https://trac.cakephp.org/ticket/1365
- Explaination of single table inheritance http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
- Martin fowler's explaination on class table inheritance http://www.martinfowler.com/eaaCatalog/classTableInheritance.html
- http://code.djangoproject.com/wiki/ModelInheritance
- http://wiki.rubyonrails.org/rails/pages/Inheritance
Note
- Haven't implement deep inheritance, ie: Dog < Pet < Mammal don't be surprise if it fails
- Associated inheritable model doesn't work since CakePHP ignore behavior callbacks beforeFind/afterFind https://trac.cakephp.org/ticket/2056
- Single-Table inheritance is quite heavily tested with association and behaviors
- Class-table inheritance used some dirty approach with some smarts, feel free to improve implmentation and test cases
- Beware performance implications and use with care
The code
Behavior and test cases are included, just extract to your cakephp root directoryhttps://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
| Field | Type |
|---|---|
| id | int |
| full_name | string |
| type | string |
| project_id | int |
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
| Field | Type |
|---|---|
| id | int |
| title | string |
| description | string |
| created | int |
| modified | int |
table images
| Field | Type |
|---|---|
| id | int |
| content_type | string |
| file_name | string |
| file_size | string |
table documents
| Field | Type |
|---|---|
| id | int |
| file_name | string |
| file_path | string |
| thumbnail | string |
table links
| Field | Type |
|---|---|
| id | int |
| url | string |
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
Comment
1 thoughts anyone?
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 :)
Bug
2 Problem
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!
Comment
3 Type for CTI
Comment
4 Storing type for CTI and deeper inheritance
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.
Comment
5 RE:Type for CTI
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
Comment
6 thanks
Comment
7 Deeper Inheritance
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
Bug
8 My bad...
Comment
9 Won't update parent model
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.
Comment
10 Possible workaround for parent associations
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:
<?phpclass 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?
Comment
11 How dos CTI work in here?
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.
Comment
12 few questions...
Model Class:
<?phpclass Asset extends AppModel {
var $belongsTo = array('User');
}
?>
Model Class:
<?phpclass 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?
Comment
13 Override CTI
In other words the Image class should have the line:
var belongsTo = array();
Comment
14 Thanks Rob
Comment
15 Thanks Rob
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?
Comment
16 type field needed when using cti
Comment
17 got that... but what about the ids?
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?
Question
18 Can't get inheritance to work
My simplified models look like this:
Model Class:
<?phpclass Person extends AppModel{
var $name = 'Person'
}
?>
Model Class:
<?phpApp::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
Comment
19 Extending CTI
Comment
20 My bad
Question
21 Where should I place the extracted files