Tag Archives: Pagination

Using MySQL INNER JOIN in CakePHP Pagination

First of all, I've got to hand it over to Matt he really did a BBBIIIGGG favor to the CakePHP community by publishing his guide to advanced CakePHP Techniques. This guide / book will give a great insight into the framework to anyone who is a seasoned programmer and is picking up Cake for the first or so time. And I'm going to be floating ideas to compliment the advanced techniques and all in all promote good programming practices of what I'm aware of.

Now, I've seen a lot of shitty code when it comes to CakePHP. Yes, I've even seen mysql_query() calls in views ! ... yes, I've lived that day and kept my sanity in tact. But I can't blame the programmers too because obviously they were newbies and were under a lot of pressure to "get things going" by their blood sucking employers. Anyhoo ... this might be the subject of another post, BUT I really had to get it out of my system ... *phew* ... feel so light now :)

So, MySQL INNER JOINS ... when should you use them ? - simple answer: when you want to filter out data in your result set. And it's quicker than filtering out results in the "WHERE" clause. Don't have any metrics to show to support this conclusion right now, but I speak in the light of many tests I've conducted on large datasets. A more simpler theory is that the "WHERE" clause needs to filter out a lot more rows in a result-set obtained as a result of using "LEFT JOIN". CakePHP's logic however is sound to use LEFT JOIN as the intention is not to filter out the records, it's merely to include whichever records belongs to the conditions you supply. That's why it's "Containable" behavior is so cool (special thanks to Felix on that for maturing it and making a part of the Cake's core).

The more you familiarize yourself with Cake's datasource classes the better. The most excellent example was published on the bakery by Nate on how to use JOINs in CakePHP. I think this should be made part of the documentation too. This could actually make you get rid of "overriding" Controller::paginate() function. When you come to know about the flexibility offerred by the datasource class you love it even more :P - a simple example:

  1.  
  2. class PostsController extends AppController {
  3.  
  4. public function by_tag ( $tag ) {
  5. /**
  6.   * This will fetch Posts tagged $tag (say, 'PHP')
  7.   */
  8. $this->paginate['Post'] = array(
  9. 'limit' => 10
  10. , 'contain' => ''
  11. , 'conditions' => array(
  12. 'Post.published' => 1
  13. )
  14. , 'fields' => array('Post.*', 'Tag.*')
  15. , 'joins' => array(
  16. 'table' => 'posts_tags'
  17. , 'type' => 'INNER'
  18. , 'alias' => 'PostTag'
  19. , 'conditions' => array(
  20. 'Post.id = PostTag.post_id'
  21. )
  22. )
  23. , array(
  24. 'table' => 'tags'
  25. , 'alias' => 'Tag'
  26. , 'type' => 'INNER'
  27. , 'conditions' => array(
  28. "PostTag.tag_id = Tag.id AND Tag.name = '$tag'"
  29. )
  30. )
  31. )
  32. );
  33.  
  34. $data = $this->paginate('Post');
  35. $this->set(compact('data'));
  36. }
  37. }
  38.  

This is just a simple example of what you can achieve by adding simple joins in your Model::find() conditions and of course in the paginate part. I've stretched it a bit further. I've actually used sub-queries and sub-joins, really complex stuff when paginating some complex data sets. Thanks to the 'joins' I never had to override the Controller::paginate() method ever. Just for the sake of example, let's say I want to retrieve posts tagged in 'PHP' and 'CakePHP' written by users who have a rating above 3. Of course this can be done in other ways, here is one using a sub-query join in CakePHP elegantly:

  1.  
  2.  
  3. // work this out wherever you want it - in your model or controller
  4. // but if I were you, I'd put this in my model
  5.  
  6. $join = "SELECT posts.id AS POST_ID FROM posts JOIN authors ON (posts.author_id = authors.id)";
  7. $join = $join.' '."JOIN users_ratings ON (authors.user_id = users_ratings.user_id AND users_ratings.rating > 3)"
  8. $join = $join.' '."WHERE 1=1";
  9.  
  10. // in your controller
  11. $this->paginate['Post'] = array(
  12. 'limit' => 10
  13. , 'contain' => ''
  14. , 'conditions' => array(
  15. 'Post.published' => 1
  16. )
  17. , 'fields' => array('Post.*', 'Tag.*')
  18. , 'joins' => array(
  19. 'table' => 'posts_tags'
  20. , 'type' => 'INNER'
  21. , 'alias' => 'PostTag'
  22. , 'conditions' => array(
  23. 'Post.id = PostTag.post_id'
  24. )
  25. )
  26. , array(
  27. 'table' => 'tags'
  28. , 'alias' => 'Tag'
  29. , 'type' => 'INNER'
  30. , 'conditions' => array(
  31. "PostTag.tag_id = Tag.id AND Tag.name IN('PHP', 'CakePHP')"
  32. )
  33. )
  34. , array(
  35. 'table' => '('.$join.')'
  36. , 'alias' => 'FILTERED_RESULTS'
  37. , 'type' => 'INNER'
  38. , 'conditions' => array(
  39. "Post.id = FILTERED_RESULTS.POST_ID"
  40. )
  41. )
  42. )
  43. );
  44.  

And this will elegantly filter out the posts you need :P

Conclusion:  you can really write any kind of a query and really devise a condition based system that would add filters auto-magically. (I will present such a system in another post) - Remember, CakePHP is all about auto-magic ! ... which is actually the culmination of "convention over configuration" so use it to the fullest !

CakePHP 1.2 Pagination explained

Today I spent a lot of time trying to figure out how to have CakePHP 1.2 pagination with all the flexibility. Because all I wanted to do was to filter the records of my model AND also unbind ALL associated models. I found the information in tits and bits a bit scattered around. So I've decided to make one spot for it, at least for my own future reference. Here is what I've understood:

There are 2 parts you have to take care of, one for the component and the other for the helper. Here is a generic example for the Post model,

/** function index () in your Posts controller **/
$this->paginate['Post'] = array(
'limit'=> 20,
'order'=> "Model.field ASC", // for example i.e.
'fields'=> array(
'Post.id',
'Post.name', //.....
),
'url'=> array(
'controller'=> 'Posts', // could be $this->name
'action'=> 'index', // could be $this->action
'created'=> '02-02-2008',
'active'=> 1, // ... modelField/Value pairs you'd like to set as filters
)
);
$posts = $this->paginate(
'Post',
array(
         "Post.created = '02-02-2008' AND Post.active = 1"
      )
);
$this->set('posts', $posts);
$this->set('paginationUrl', array('url'=>$this->paginate['Post']['url']));


This will let the component do the math and calculate how many rows are being returned. Since pagination component executes a simple "findAll()", so it will treat all modelField / Value pairs it finds in the URL key as search criteria i.e. conditions just like you'd specify conditions as array in any "findAll()" call.

To keep or switch between the modelField / Value pairs throughout the pages, you can pass these as URL parameters. So your URL should look kind of like this:

http://yourhost/cake_root/controller/action/page:2/modelField:value/modelField:value/ ....

Now for the pagination helper, we'd use $paginationUrl array that we set for our view:

/** after rendering all posts **/
pr($posts);   /* or you can use Cake's built in function to display the $posts array  */
echo $paginator->prev("<< Prev", array("url"=>$paginationUrl));
echo $paginator->numbers(array("url"=>$paginationUrl));
echo $paginator->next("Next >>", array('url'=>$paginationUrl));


And that's it, now the paginator links would render your given modelField / Value pair. But there is or "was" a downside about it that my db fields would be seen via the URL. For example, the URL for the above action would be like this:

root/controller/action/page:2/created:02-02-2008/active:1

and that was unacceptable for me, so I created a masking scheme for my field / value pairs. May be I'd make a component out of it, but that won't be as generic as you'd still have to pass the Masks / Fields pairs as an array.

Ahh !! so what about unbinding associated models ? ... well ... what I do is that I use the cool unbindAll function by Othman Ouihibi. I've been using it since CakePHP 1.1 days. But there's a behavior that supports this functionality too on the Bakery, pretty cool. So I just unbindAll models except the ones I need before every paginate call in my controller and that does the trick. Any suggestions / comments would be highly appreciated.

This article was originally made by collecting information from the comments here, so I'd strongly advise anyone who wants to know advance paging to read that article.

HAPPY BAKING !!!