Jan 23rd, 2008
Патч belongsTo в CakePHP - поиск по нескольким уровням таблиц
Модели CakePHP при выборке через findAll() умеют подключать "LEFT JOIN"-ом только таблицы первого уровеня связанные через belongsTo. Данные остальных уровней выбираются отдельными запросами для каждой выбранной записи. Это не даёт возможности осуществлять поиск и сортировка по полям этих таблиц.
Рассмотрим на стандартном блоговском примере categories - posts - comments:
class Category extends AppModel {
}
class Post extends AppModel {
var $belongsTo = array(
‘Category’ => array(
‘className’ => ‘Category’,
‘foreignKey’ => ‘category_id’,
),
);
}
class Comment extends AppModel {
var $belongsTo = array(
‘Post’ => array(
‘className’ => ‘Post’,
‘foreignKey’ => ‘post_id’,
),
);
}
Делая $Comment->findAll() мы, например, не сможем отсортировать результаты по Category.name. Однако если попробовать связать напрямую Comment с Category то модель приJOINит таблицу Category:
class Comment extends AppModel {
var $belongsTo = array(
‘Post’ => array(
‘className’ => ‘Post’,
‘foreignKey’ => ‘post_id’,
),
‘Category’ => array(
‘className’ => ‘Category’,
‘foreignKey’ => ‘category_id’,
),
);
}
В результате $Comment->findAll() построит такой запрос:
SELECT `Category`.`name` FROM `comments` AS `Comment` LEFT JOIN `posts` AS `Post` ON (`Comment`.`post_id` = `Post`.`id`) LEFT JOIN `categories` AS `Category` ON (`Comment`.`category_id` = `Category`.`id`) ORDER BY `Category`.`name` ASC
который вызовет ошибку SQL из-за того, что модель всегда подставляет имя мастер-таблицы в связывающем условии, вместо `Comment`.`category_id` должно быть `Post`.`category_id`. Здесь не обойдётся без небольшого изменения в исходниках CakePHP.
Патч для CakePHP
(новый параметр для $belongsTo)
Патчить CakePHP для меня уже давно (ещё с нулевой версии) стало привычной практикой. Конечно правильней постить ошибки в багтреккер и ждать пока разработчикивнесут изменения и выпустят новый релиз, но это долго. Гораздо быстрее написать патчик и двигаться дальше (паралельно написав разработчикам, если есть время). Большая часть изменений которые я делал в конечном счёте были сделаны в следующих версиях, так что в моих проектах количество патчай остаётся неизменным - 3-4, добавляю новые, удаляю старые и так от версии к версии.
Для решения поставленной задачи я добавил новый параметр ‘foreignTable’ в массив $belongsTo. Теперь моя модель выглядит так:
class Comment extends AppModel {
var $belongsTo = array(
‘Post’ => array(
‘className’ => ‘Post’,
‘foreignKey’ => ‘post_id’,
),
‘Category’ => array(
‘className’ => ‘Category’,
‘foreignTable’ => ‘Post’,
‘foreignKey’ => ‘category_id’,
),
);
}
Теперь в связывающем условии используется правильное имя таблицы. Таким чудо-методом я связал 5 уровней таблиц у себя в проекте не прибегая к использованию query() и построению запросов вручную.
Ну а теперь непосредственно сам патч. Я всегда стараюсь патчить как можно меньше строк, чтобы затем легко переносить изменения на новые релизы кейка. В данном случае я ограничелся изменением всего орной строки в файле \cakephp\v1.1.18.5850\libs\model\datasources\dbo_source.php:
1. Находим функцию generateAssociationQuery()
2. Скролим несколько страниц вниз и находим блок
switch($type) {
case ‘hasOne’:
case ‘belongsTo’:
if ($external) {
…
} else {
if ( $type == ‘hasOne’ ) {
…
} elseif ( $type == ‘belongsTo’ ) {
// Эту строку и будем патчить
$conditions = $this->__mergeConditions(…);
//
}
…
}
break;
3. Заменяем строку на
$conditions = $this->__mergeConditions($assocData[’conditions’], array("".(@$assocData[’foreignTable’]?$assocData[’foreignTable’]:$model->name).".{$assocData[’foreignKey’]}" => ‘{$__cakeIdentifier[’ . "{$alias}.{$linkModel->primaryKey}" . ‘]__$}’));
4. Enjoy!
PS. Не забываем помечать свои патчи каким-нть унифицированным комментарием, чтобы легко находить и переносить их в новые версии CakePHP.