Модели 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.

Trackback URI | Comments RSS

Leave a Reply