Recursive Association Support in CakePHP

We have all been there. A simple query but the query condition is three joins away. Now I don't have to build a large custom query.

Assume you have the models User, City, State, Area, Country, Region. Users are in a City. Cities are in a State. States are in an Area. Areas are in a Country. Countries are in a Region. How do you query for Users in a Country? Or Area? The query is simple but Cake has not supported it. This feature is listed as issue #633 in Trac.

After discussing the matter with PHPNut, we decided the method of navigation through the associations should be via :: operators. So to limit users by Country would be

City::State::Area::Country.name

A query using findAll would then be like

$users = $this->User->findAll( array('City::State::Area::Country.name'=>'Canada') );

The query formed would look something like


SELECT `User`.`id`,`User`.`name`,`User`.`email`,`User`.`city_id`, 
`City`.`id`,`City`.`name`,`City`.`state_id`,
`City::State`.`id`,`City::State`.`name`,`City::State`.`area_id`,
`City::State::Area`.`id`,`City::State::Area`.`name`,`City::State::Area`.`country_id`,
`City::State::Area::Country`.`id`,`City::State::Area::Country`.`name`,`City::State::Area::Country`.`region_id`,
FROM `users` AS `User`
LEFT OUTER JOIN `cities` AS `City` ON `User`.`city_id`=`City`.`id`
LEFT OUTER JOIN `states` AS `City::State` ON `City`.`state_id`=`City::State`.`id`
LEFT OUTER JOIN `areas` AS `City::State::Area` ON `City::State`.`area_id`=`City::State::Area`.`id`
LEFT OUTER JOIN `countries` AS `City::State::Area::Country` ON `City::State::Area`.`country_id`=`City::State::Area::Country`.`id`
WHERE `City::State::Area::Country`.`name` = 'Canada'

I have patched dbo_source.php to behave this way. Unfortunately, I have not had the time to work out certain situation where this does not work. The first, and most important, item that does work is the normal CakePHP queries. I have taken great pains to ensure the normal queries work the same with almost no loss of speed.

That said, this technique has only had success with hasOne, hasMany, and belongsTo associations that are in the same database. No external associations and no hasAndBelongsToMany associations work. Once you specify a recursive association using the :: operator, the query will fail if it encounters an external or HABTM association. It also has some issues with the condition being in a string instead of array entries. I will be developing more generic unit tests for this including its deficiencies so that incremental development on them can occur later.

So far these limitations have proved to not be a problem for me and in any case would be solvable using traditional techniques.

Here is the source for version 1.2.0.4451 - dbo_source.php and the unified patch difference dbo_source.patch.

Here is the source for version 1.1.12.4202 - dbo_source.php and the unified patch difference dbo_source.patch.

The dbo_source.php file lives in /cake/libs/model/datasources

Trackbacks

    No Trackbacks

Comments

Display comments as (Linear | Threaded)

  1. othman ouahbi aka CraZyLeGs says:

    Nice, I had specified '.' as the models separator in that ticket but :: is no issue. it mimics the scope operator.

    Nice work.

  2. Martin says:

    Looks great, unfortunately there is a syntax error in PHP4 on line 1628, '&$out=array()'.

    Also, the Call-time pass-by-reference arguments are marked as deprecated in my version of PHP4.

    So it looks like this is a PHP5 fix only.

  3. gosciu says:

    It works great (except references, which shouldn't be in function call) when You looking for certain value of associated model ('City::State::Area::Country.name' = 'Canada' in this particular example) but it doesn't work when You only want to join by association (fetch City::State::Area::Country.name, by condition applied to eg User).


The author does not allow comments to this entry