Topic: HasMany and BelongsTo issues and ideas
author: Arialdo Martini
created: 10-12-2008 07:58:29 AM
|
I tried to use the new features HasMany() and BelongsTo(), introduced in ADOdb 5.06.
They are great: with those new features, Active Record is now closer than before to an ordinary ORM.
Unfortunately, I found two issues.
I also tried to write a kind of sample-extension, which I propose as an idea for further enhancements.
Suppose you have two tables, "users" and "posts". Each "user" can have many "posts".
You can write:
class ADO_user extends ADOdb_Active_Record{};
class ADO_post extends ADOdb_Active_Record{};
$user1 = new ADO_user('users');
$user2 = new ADO_user('users');
$user1->Load("id=1");
$user2->Load("id=2");
Since I never defined a HasMany relation
$user1->posts
and
$user2->posts
are not defined.
I must explicitely call
$user1->HasMany('post','user_id');
$user2->HasMany('post','user_id');
for both instances, then I can access users' posts.
First doubt: relation between "users" and "posts" should be defined in the class definition, since *every* "user" can have many "posts". Actual ADOdb's approach forces the user to explicitely define relations between table for each instance. What if relations change? The developer must modify a lot of lines of code.
To me, a smarter approach could be defining relations between tables (that is, between classes) in the classes themselves, like in
class ADO_user extends ADOdb_Active
{
var $hasMany = array(
'post' => array(
'foreignKey' => 'user_id',
'tableName' => 'posts',
'className' => 'post'
)
);
};
Doing this, each "user" instance already knows it "hasMany" posts, and no further code is required. It would be more "incapsulated", more clear, less redundant.
The second issue is related to the class instantiated by ADOdb for a hasMany relation.
Since the relation is defined in terms of tables, and not in terms of classes, when ADOdb accesses to user's posts, it instatiates an ordinary ADOdb_Active_Record: but for the developer's point of view, a post is an "ADO_post" class, extending ADOdb_Active_Record.
For example, if the developer has defined some extra methods or attributes in "ADO_post" class, he or she won't be able to see them in $user->posts, since $user->posts[0] is a simple ADOdb_Active_Record and not an ADO_post (extending ADOdb_Active_Record) instance.
This can be seen with a simple get_class againt $user1->posts[0], for example.
In my opinion, this is not the best choice, because usually developers define a lot of methods in their ADO classes (for data validations or for any other reason).
In addiction to that, if "post" has other relations, the developer must define them:
$user1->HasMany('post','user_id');
foreach($user1->posts as $post)
$post->HasMany('comment','post_id');
This is uncomfortable as well, because "post" should already know it "hasMany" "comments"
I tried to override all of those limitations with a sample, rudimental, simple, experimental code: I'm not proposing it as a patch but as a simple example of my ideas. Please, note that my code doesn't deal with eventual circular relations_ CakePHP solves this stopping the relations depth to a fixed number, my code does nothing and probably goes to an overflow :(
In order to mantain backwards compatibility I'm not changing the ADODB_Active_Record class but just adding an addictional method:
In the file adodb-active-record.inc.php you could add:
function hasManyExtended($class, $foreignRef, $foreignKey = false)
{
eval("\$ar = new {$class}('$foreignRef');");
$ar->foreignName = $class;
$ar->UpdateActiveTable();
$ar->foreignKey = ($foreignKey) ? $foreignKey : $foreignRef.ADODB_Active_Record::$_foreignSuffix;
$table =& $this->TableInfo();
$table->_hasMany[$foreignRef] = $ar;
}
Differently than the original function hasMany, it requires the class name (in our case, "post") as argoument.
Note as the function instantiates a "class name" instance and not a ADOdb_Active_Record instance.
Again, since I don't want to corrupt the original ADOdb_Active_Record class, in my example, I'm using a different class, extending ADOdb_Active_Record. I will use that class to define relations between tables, like that:
class ADOdb_Active_Record_Extended extends ADOdb_Active_Record
{
function __construct($table = false, $pkeyarr=false, $db=false)
{
parent::__construct($table, $pkeyarr, $db);
if(isset($this->hasMany))
{
foreach($this->hasMany as $hasmany)
{
$this->HasManyExtended($hasmany['className'], $hasmany['tableName'],$hasmany['foreignKey']);
}
}
}
}
Now I can define the "user" class with relations definitions as well: they will be already defined for any instances of the class
class ADO_user extends ADOdb_Active_Record_Extended
{
var $hasMany = array(
'post' => array(
'foreignKey' => 'utente_id',
'tableName' => 'posts',
'className' => 'post'
)
);
};
Now, when I instantiate an ADO_user, HasManyExtended is called for each relations.
$user1 = new ADO_user('users');
$user2 = new ADO_user('users');
$user1->Load("id=1");
$user2->Load("id=2");
The differences are:
- $user1->posts $user2->posts are now already defined
- $user1->posts[0] is a "post" instance, not an ordinary "ADOdb_Active_Record" instance. Anything defind in class "post" is available. Even further relations.
Obviously, this is just an idea: as you see, this code is very rudimental. Nevertheless I wonder if this approach can be useful for developers. I am used to CakePHP's ORM and to Doctrine. I know ADOdb is not meant to be an ORM: but since it features HasMany and BelongsTo, I really think it should define relations in the class rather than in the instance and it should instantiate business object's instances rather than ordinary ADOdb_Active_Record's instances.
My 2 cents. |
|
Topic: Re:HasMany and BelongsTo issues and ideas
author: Chris Ravenscroft
created: 11-12-2008 03:12:13 AM
|
Arialdo,
Thanks for this thoughtful post and letting me know about it by email.
First, I would like to point out that v5.06 comes with two active records files:
adodb-active-record.inc.php
adodb-active-recordx.inc.php
The one I am most familiar with is the 'x' -- as in 'extended' -- version because it's the result of many back and forths between John and me.
The 'non x' version is simpler, more in line with adodb's original philosophy.
I recommend you take a look at the test harnesses in tests/test-active-relationsx.php
You will see that I typically declare relations in my classes' constructor, as you suggest. You will also see that I use alias classes when I need to ignore a relation.
Why? Because, due to the heavy use of metadata, the relations, once defined, are shared by all the instances of a class (slight simplification here, I hope you will forgive).
These classes can then be connected implicitly using field_id names, if, like me, you favour convention over definition.
If, like John, you prefer definition over convention, or if you wish to use an already existing schema, you can decide to explicitly name your foreign keys.
Note that you can specify which load method you wish to use, eg 'lazy', 'join' or 'worker.' I tend to prefer 'worker' as its cost is linear with the number of relations. If you have defined your relations in your class' constructor and use the 'worker' query mode, a var_export() call will show that all that you need was immediately loaded.
Regarding your second point, you are absolutely right. I had a first implementation that did just what you mention but I dropped it, for now, for the following reasons:
1. I needed to decide what to do with dependent objects: was I going to also retrieve their relations right away? Or what mechanism could I use to decide on which method to use? ('lazy', 'worker', 'join'?)
2. How did I know which class to use? Did I define a pseudo classpath and every time I found a dependent I would look it up in my classpath? Note that it's actually something I could do with the framework that I have been working on (http ://github.com/Fusion/lenses/)
3. I have not had time, yet, to decide how to handle "chain reactions" -- what happens when the database load explodes due to too many relations chained? I have not put it down on paper yet but I think that there's a potential for O(n2) here.
I would actually appreciate your input on these points.
-Chris.
ps: after reading your code, it seems to me that you would suggest defining relations in a flat structure rather than tie it to each class' constructor? Am I correct? |
|
Topic: Re:HasMany and BelongsTo issues and ideas
author: Arialdo Martini
created: 11-12-2008 07:04:35 AM
|
>Arialdo,
>Thanks for this thoughtful post and letting me know about >it by email.
*Thank you* for your time and your clear answer.
>First, I would like to point out that v5.06 comes with two
>active records files:
>adodb-active-record.inc.php
>adodb-active-recordx.inc.php
My fault: I didn't know about the extended version and it's a big news for me. That's great and I'm going to study it.
It seems to cover much more of the topics discussed in my post. I'm sorry I haven't noticed it. Probably, my post is completely useless now that I know there's adodb-active-recordx.inc.php.
>3. I have not had time, yet, to decide how to handle
>"chain reactions" -- what happens when the database load
>explodes due to too many relations chained? I have not put
>it down on paper yet but I think that there's a potential
>for O(n2) here.
yes, this is a big problem, unless you just stick to lazy load. Some frameworks that don't support lazy loading solve the problem in a very unelegant way, stopping to a fixed depth, which can be very unfair if you need to access very nested informations.
>I would actually appreciate your input on these points.
Yep, thank you. As far as I understand I underestimated the power of ADOdb, due to my ignorance over the extended version. I'm going to use it. Thank you very much for your help
>ps: after reading your code, it seems to me that you would
>suggest defining relations in a flat structure rather than
>tie it to each class' constructor? Am I correct?
Yes, definitely.
Like you, I'm a fan of conventions over definitions.
I think a flat structure for defining relations can be more confortable, because you just have to stick to a convention defining your structure and all the dirt job is done by the parent constructor; you can store your structure in a variable, print it out, or even let it be built by an external method (for example, by a method which dynamically analizes the db structure and returns the right flat structure to be passed to the class: I once wrote such a method to have models' relations always corresponding to the tables' associations - this can be useful in the very first stages of the developing when you may find yourself changing your db several times).
A flat structure can be easily serialized and converted (you can even think about a method to convert the structure to an SQL script able to generate the db, like Hibernate does).
To ignore a relation you can think about a method to be called in the constructor (something like UnBind()). Nothing stops you to have a flat structure to define relations AND methods to update them on the fly (that is, something like a BindTo() and UnBind(), which may accept a single relation or a set of relations, given with the same convention of the flat structure).
I mean, something like
$this->bindTo(
array(
'HasMany'=> array(
'post' => array(
'foreignKey' => 'user_id',
'tableName' => 'posts',
'className' => 'post'
),
'BelongsTo'=> array(
'category' => array(
'foreignKey' => 'category_id',
'tableName' => 'categories',
'className' => 'category'
)
)
);
Just to make you an example, it's very similar to CakePHP's approach in linking models together. It uses an array to store every kind of relation
http ://book.cakephp.org/view/78/Associations-Linking-Models-Together
and it also allows the use of bind and unbind methods.
Thank you again, I'll start studying your extended version.
have a nice day |
|
Topic: Re:HasMany and BelongsTo issues and ideas
author: John Lim
created: 11-12-2008 08:54:26 AM
|
Arialdo
Thanks for the interesting post.
(1) Support for defining table relationships. ADOdb stores table metadata already in the ADODB_Active_Table structure. Here are some new functions defined in the next release to support this:
// use when you don't want ADOdb to auto-pluralize tablename
static function TableHasMany($table, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
{
$ar = new ADODB_Active_Record($table);
$ar->hasMany($foreignRef, $foreignKey, $foreignClass);
}
// use when you want ADOdb to auto-pluralize tablename for you:
// e.g. class Person will generate relationship for table Persons
static function ClassHasMany($parentclass, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
{
$ar = new $parentclass();
$ar->hasMany($foreignRef, $foreignKey, $foreignClass);
}
You can then call
class Person extends ADODB_Active_Record { }
ADODB_Active_Record::ClassHasMany('Person', 'Children', 'person_id');
$p = new Person();
$p->Load('id=1');
foreach ($p->children as $child)
var_dump($child);
(2) To support child class, use
function hasMany($foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record')
{
$ar = new $foreignClass($foreignRef);
$ar->foreignName = $foreignRef;
$ar->UpdateActiveTable();
$ar->foreignKey = ($foreignKey) ? $foreignKey : $foreignRef.ADODB_Active_Record::$_foreignSuffix;
$table =& $this->TableInfo();
$table->_hasMany[$foreignRef] = $ar;
# $this->$foreignRef = $this->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get()
}
Note new parameter at end of hasMany(). This is also supported in TableHasMany() and ClassHasMany(). |
|
Topic: Re:HasMany and BelongsTo issues and ideas
author: Arialdo Martini
created: 11-12-2008 09:21:56 AM
|
Thanks! I think have everything to use ADOdb and enjoy relations.
Thank you again for the support and the clarifications.
PS. And thanks for the tip
$ar = new $parentclass();
which is much better than my awful use of eval(). I didn't know that. |
|
Topic: Re:HasMany and BelongsTo issues and ideas
author: Chris Ravenscroft
created: 14-12-2008 05:15:28 AM
|
Oh that's awesome. Now, you can definitely choose your poison: convention or definition :)
Arialdo, if you enjoyed $parentclass(), then you should definitely have a look at amusing things such as $$variable
-C. |
|
|