태그 : CakePHP

HABTM Pagination on CakePHP 1.2x

게시판 따위를 만들 때마다 항상 귀찮게 하는 pagination 작업을 CakePHP 1.2 에서는 paginator helper 를 통해서 가볍게 지원해준다.

Post 를 페이지별로 보여주고 싶을 때, 간단히 몇줄로 작업 끝이다.

class PostsController extends AppController {
  
    var $paginate = array( 'limit' => 5, 'order' => array( 'Post.created' => 'desc' ) );

    function idex()
    {
        $rs = $this->paginate( 'Post', array( 'Post.status' => '>0' ) );
        debug( $rs ); exit;
    }
}


그런데, 여기에 HABTM association 이 들어가게 되면 문제가 좀 더 복잡해진다.

class Post extends AppModel {
    var $hasAndBelongsToMany = array( 'Tag' => array( 'conditions' => 'PostsTag.status > 0' ) );
}


이렇게 Tag 과 HABTM 관계를 정의해놓고 위의 index 를 그대로 호출하면 아름답게 Post 에 붙은 Tag 들이 끌려 나온다.

Array
(
    [0] => Array
        (
            [Post] => Array
                (
                    [id] => 1
                    [status] => 1
                    [created] => 2007-11-18 23:49:59

                    [text] => hello
                )
            [Tag] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [text] => testTag
                            [PostsTag] => Array
                                (
                                    [post_id] => 1
                                    [tag_id] => 1
                                    [status] => 1
                                )
                        )
                    [1] => Array
                        (
                            [id] => 13
                            [text] => hello
                            [PostsTag] => Array
                                (
                                    [post_id] => 1
                                    [tag_id] => 13
                                    [status] => 1
                                )
                        )
            )
)

뭐 아직까지는 별다른 작업 없이도 원하는 결과를 얻을 수 있다.

참고로 이 때 실제로 수행되는 query 는 아래와 같다

SELECT COUNT(*) AS `count` FROM `posts` AS `Post` WHERE `Post`.`status` > 0        // pagination 을 위한 count 처리
SELECT `Post`.`id`, `Post`.`status`, Post`.`created`, `Post`.`text` FROM `posts` AS `Post` WHERE `Post`.`status` > 0 ORDER BY `Post`.`created` desc LIMIT 5
SELECT `Tag`.`id`, `Tag`.`text`, `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status` FROM `tags` AS `Tag` JOIN `posts_tags` AS `PostsTag` ON (`PostsTag`.`post_id` IN (1, 2) AND `PostsTag`.`tag_id` = `Tag`.`id`) WHERE `PostsTag`.`status` > 0
    // 각 Post 에 해당하는 Tag 검색


자, 여기까지는 무난하다.
그럼 이번에는 Tag 별 검색을 해보도록 하자.
원하는 결과는 아래와 같다

INDEX BY 'testTag'

Post1
    tags : testTag, hello, tag1

Post4
    tags : testTag

Post11
    tags : asdf test testTag


즉, 간단하게 말하면 특정 tag 을 포함하는 게시물의 리스트를 보여주는 것이다.

사실 이것을 처리하는 데에는 여러가지 방법이 있다.

posts_tags 테이블에서 tag_id 로 query 한 결과를
$this->paginate( 'Post', array( 'Post.status' => '>0', 'Post.id' => 'IN (' . implode( ', ', $list ) . ')' ) );
해서 처리해도 되지만, 뭔가 깔끔해보이지 않는다.

paginate HABTM 을 구글해보면 몇가지 글이 검색된다.
참고할만한 것들은 이것
http://cakebaker.42dh.com/2007/10/17/pagination-of-data-from-a-habtm-relationship/
http://www.cricava.com/blogs/index.php?blog=6&title=modelizing_habtm_join_tables_in_cakephp_&more=1&c=1&tb=1&pb=1

두번째 링크에 있는 mariano iglesias 의 방법을 살짝 바꿔서 써보도록 하자.
여기 나온 것은 일단 paginate 를 사용하지 않고 findAll 만 호출했고, 각 게시물별 tag 이 결과에 포함되어 있지 않기도 하고, 하여간 직접 쓰기에는 적당하지 않다.

function tags( $tagid )
{
    $rs = $this->paginate( 'PostsTag', array( 'PostsTag.tag_id' => $tagid ) );
    debug( $rs );
}


아이쿠 $paginate 를 정의해준 데서 사용한 'order' => array( 'Post.created' => 'desc' )  부분이 문제다. 실제 쿼리를 보면

SELECT COUNT(*) AS `count` FROM `posts_tags` AS `PostsTag` WHERE `PostsTag`.`tag_id` = 17
SELECT `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status` FROM `posts_tags` AS `PostsTag` WHERE `PostsTag`.`tag_id` = 17 ORDER BY `Post`.`created` desc LIMIT 5


이렇게 되어있다. PostsTag 과 Post 간의 관계가 정의되어 있지 않아서 생기는 문제다.
살짝
$this->paginate = array( 'limit' => 2 );
라고 끼워넣어보면 결과가 나오기는 하지만, 역시 원하는 결과는 아니다.

자, 그러면 여기서 PostsTag 과 Post 간의 관계를 정의해버리면 되겠다.
$this->Post->PostsTag->bindModel( array( 'belongsTo' => array( 'Post' ) ) );
$rs = $this->paginate( 'PostsTag', array( 'PostsTag.tag_id' => $tagid ) );


어라? 여전히 똑같은 에러가 나온다. 하지만 실제 쿼리는 살짝 다르다.

SELECT COUNT(*) AS `count` FROM `posts_tags` AS `PostsTag` LEFT JOIN `posts` AS `Post` ON (`PostsTag`.`post_id` = `Post`.`id`) WHERE `PostsTag`.`tag_id` = 17
SELECT `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status` FROM `posts_tags` AS `PostsTag` WHERE `PostsTag`.`tag_id` = 17 ORDER BY `Post`.`created` desc LIMIT 5


두번째 쿼리는 동일하지만 첫번째 쿼리에 posts 테이블이 (바라던 대로) left join 되었다.
bindModel(), unbindModel()은 이후에 시행되는 쿼리 한번에 대해서만 유효한데, paginate() 안에서 쿼리를 두번 하기 때문에 두번째 쿼리에는 적용이 되지 않기 때문이다.

원인을 알았으니 다시 해보자.

$this->Post->PostsTag->bindModel( array( 'belongsTo' => array( 'Post' ) ), false );
$rs = $this->paginate( 'PostsTag', array( 'PostsTag.tag_id' => $tagid ) );


bindModel() 에서 두번째 인자로 false 를 주면 한번 쿼리 한 이후에도 bind/ unbind 가 해제되지 않는다.
자, 이제 결과가 나왔다.

Array
(
    [0] => Array
        (
            [PostsTag] => Array
                (
                    [post_id] => 34
                    [tag_id] => 17
                    [status] => 1
                )

            [Post] => Array
                (
                    [id] => 34
                    [status] => 1
                    [created] => 2007-11-18 23:49:59
                    [modified] => 2007-11-20 01:28:41
                    [text] => 그렇다.
                )
        )

    [1] => Array
        (
            [PostsTag] => Array
                (
                    [post_id] => 29
                    [tag_id] => 17
                    [status] => 1
                )

            [Post] => Array
                (
                    [id] => 29
                    [status] => 1
                    [created] => 2007-11-17 20:08:35
                    [text] => 에헤라 얼씨구 절씨구 차차차 얼쑤~
                )
        )
)

자, 원하던대로 TAG 17 을 가지는 POST (29, 34) 가 반환되었다.
아니 그런데 잘 살펴보니, tag 은 어디갔어?

위의 mariano iglesias 의 방법대로
$this->Post->PostsTag->bindModel( array( 'belongsTo' => array( 'Post', 'Tags ) ), false );
해주면 어떨까?

Array
(
    [0] => Array
        (
            [PostsTag] => Array
                (
                    [post_id] => 34
                    [tag_id] => 17
                    [status] => 1
                )

            [Post] => Array
                (
                    [id] => 34
                    [status] => 1
                    [created] => 2007-11-18 23:49:59
                    [text] => 그렇다.
                )

            [Tag] => Array
                (
                    [id] => 17
                    [text] => 테스트
                )
        )

    [1] => Array
        (
            [PostsTag] => Array
                (
                    [post_id] => 29
                    [tag_id] => 17
                    [status] => 1
                )

            [Post] => Array
                (
                    [id] => 29
                    [status] => 1
                    [created] => 2007-11-17 20:08:35
                    [text] => 에헤라 얼씨구 절씨구 차차차 얼쑤~
                )

            [Tag] => Array
                (
                    [id] => 17
                    [text] => 테스트
                )
        )
)

오. tag 정보까지 나왔다.
그런데 잘 살펴보면 이것은 원하는 결과가 아니다. 그 게시물에 포함된 모든 tag 가 반환된 것이 아니라 id=17 인 tag 정보만 포함되어서 나왔다.
자, 그럼 원하는 결과를 얻기 위해서는 어떻게 해야할까? 아래와 같이 가볍게 recursive 만 하나 더해주면 된다.

$this->Post->PostsTag->bindModel( array( 'belongsTo' => array( 'Post' ) ), false );
$this->Post->PostsTag->recursive = 2;
$rs = $this->paginate( 'PostsTag', array( 'PostsTag.tag_id' => $tagid ) );


결과는 아래와 같다

Array( 
[0] => Array (
[PostsTag] => Array (
[post_id] => 34
[tag_id] => 17
[status] => 1
)
[Post] => Array (
[id] => 34
[status] => 1
[created] => 2007-11-18 23:49:59
[text] => 그렇다.
[Tag] => Array (
[0] => Array (
[id] => 2
[text] => asdf
[PostsTag] => Array (
[post_id] => 34
[tag_id] => 2
[status] => 1
)
)
[1] => Array (
[id] => 17
[text] => 테스트     
[PostsTag] => Array (
[post_id] => 34
[tag_id] => 17
[status] => 1
)
)
)
)
)
[1] => Array (
[PostsTag] => Array (
[post_id] => 29
[tag_id] => 17
[status] => 1
)
[Post] => Array (
[id] => 29
[status] => 1
[created] => 2007-11-17 20:08:35
[text] => 에헤라 얼씨구 절씨구 차차차 얼쑤~
[Tag] => Array (
[0] => Array (
[id] => 16
[text] => 타령     
[PostsTag] => Array (
[post_id] => 29
[tag_id] => 16
[status] => 1
)
)
[1] => Array (
[id] => 17
[text] => 테스트     
[PostsTag] => Array (
[post_id] => 29
[tag_id] => 17
[status] => 1
)
)
)
)
)
)

자, 이제는 정말 원하는 결과가 나왔다.
Tag 밑에 필요없는 PostsTag 까지 따라나오긴 했지만, 이건 그냥 무시하면 된다.

실제 query는 아래와 같다.

SELECT COUNT(*) AS `count` FROM `posts_tags` AS `PostsTag` LEFT JOIN `posts` AS `Post` ON (`PostsTag`.`post_id` = `Post`.`id`) WHERE `PostsTag`.`tag_id` = 17
SELECT `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status`, `Post`.`id`, `Post`.`status`, `Post`.`created`,  `Post`.`text` FROM `posts_tags` AS `PostsTag` LEFT JOIN `posts` AS `Post` ON (`PostsTag`.`post_id` = `Post`.`id`) WHERE `PostsTag`.`tag_id` = 17 ORDER BY `Post`.`created` desc LIMIT 5
SELECT `Post`.`id`, `Post`.`status`, `Post`.`created`, `Post`.`text` FROM `posts` AS `Post` WHERE `Post`.`id` = 34
SELECT `Tag`.`id`, `Tag`.`text`, `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status` FROM `tags` AS `Tag` JOIN `posts_tags` AS `PostsTag` ON (`PostsTag`.`post_id` IN (34) AND `PostsTag`.`tag_id` = `Tag`.`id`) WHERE `PostsTag`.`status` > 0
SELECT `Post`.`id`, `Post`.`status`, `Post`.`created`, `Post`.`text` FROM `posts` AS `Post` WHERE `Post`.`id` = 29
SELECT `Tag`.`id`, `Tag`.`text`, `PostsTag`.`post_id`, `PostsTag`.`tag_id`, `PostsTag`.`status` FROM `tags` AS `Tag` JOIN `posts_tags` AS `PostsTag` ON (`PostsTag`.`post_id` IN (29) AND `PostsTag`.`tag_id` = `Tag`.`id`) WHERE `PostsTag`.`status` > 0

recursive 때문에 검색된 Post 갯수 * 2 만큼 쿼리 수가 늘어나는 문제는 있지만, 일단 결과는 무난히 처리되었다.
만약 이렇게 쿼리 수가 늘어나는 것이 싫다면 recursive=1 의 결과를 취합해서 해당 Post 와 연관되는 Tag 들을 모두 검색하여 다시 연관지어주는 작업을 손으로 해줘야 한다.
어느쪽이 좋을지는 각자의 판단대로 : )

by mine | 2007/11/22 11:05 | | 트랙백 | 덧글(0)

◀ 이전 페이지 다음 페이지 ▶