Howto create a counter cache in symfony

June 8, 2007

What’s a counter cache? I think that the Ruby on Rails wiki explains this best: “It caches the number of belonging objects on the associated class.
Think about something like a weblog with posts and comments. You may want to display the number of comments each post has. Normally you would execute a query to count them, but with a counter cache the post table would have an extra field which updates every time a new comment is created or deleted. This way you save quite a few SQL queries.

While in symfony it isn’t that simple as in RoR, but it is relatively easy to do. Let’s take a look at the schama.yml first:

 

propel:
  post:
    id:
    title: varchar(255)
    body: longvarchar
    comment_count: integer
    created_at:

  comment:
    id:
    post_id:
    body: longvarchar
    author: varchar(50)
    created_at:

Notice the comment_count field in the post table, which will cache the number of associated comments to each post.
Build the model and open up the lib/model/Comment.php file in your editor. We need to override the save method and make sure that every time a comment is saved, the comment_count field of the associated post is updated:

public function save($con = null) 
{
  if ($this->isNew())
  {
    $this->getPost()->updateCommentCount();
  }
 
  return parent::save();
}

First we check with the isNew method if a comment really is a new comment because the save method is also used to save an modified object, and then we call the updateCommentCount method of the associated post to update the comment count.

Open the lib/model/Post.php file and create the updateCommentCount method:

public function updateCommentCount()
{
  $this->setCommentCount($this->getCommentCount() + 1);
}

And that’s it, now everytime a new comment is created, the comment_count field of the associated post is increased by one. You don’t have to worry about it anymore.

But there is still one thing missing: to decrease the comment_count field of an associated post each time a comment is deleted.

This isn’t a problem because the basics are the same: override the delete method of the Comment model and modify the updateCommentCount method of the Post model (or create increaseCommentCount / decreaseCommentCount methods if you like that more):

Here’s a example:

public function delete($con = null)
{
  $this->getPost()->updateCommentCount(false);
  
  return parent::delete();
}

As you can see i also modified the updateCommentCount method to handle both (in- and decreasing) cases:

public function updateCommentCount($increase = true)
{
  $count = $this->getCommentCount();
  $count = ($increase) ? $count + 1 : $count - 1;
  $this->setCommentCount($count);
  $this->save();
}

If i pass true as a parameter the comment_count field is increased by one and if a pass false it decreases the field by one. (Notice that you have to call the save method when you override the delete method because otherwise it wouldn’t save the updated count. It wasn’t necessary in the save method because the it already returned parent::save)

I also recommend watching this Railscast episode about counter cache, its not PHP but you get the clue.

All comments are appreciated.

Advertisements

15 Responses to “Howto create a counter cache in symfony”

  1. z01d Says:

    Thank you for the snippet, Arthur.
    The askeet tutorial does something similar here with transactions: http://www.symfony-project.com/askeet/4


  2. Good tip, but it’s similar to Askeet tutorial’s “Modify the save() method of the Interest object” section:

    http://www.symfony-project.com/askeet/4

  3. evan Says:

    Looks good – I’m just starting to implement something like this in order to cut down db requests counting posts/comments in a directory of blogs. The thing I’m wondering about is dealing with simultaneous updates. If two comments are submitted near-simultaneously the objects could each increment their count values unaware of the other comment – so your count would end up one short. It’s probably a rare occurance but I’m worried that as traffic grows it’ll become more and more likely.


  4. In your overridden save/delete methods you don’t pass the $con object through. It’s nice to know the default is null (therefore in the majority of cases it won’t be used), but as it’s a parameter you should pass it on – someone may be using it…

  5. Markus Says:

    andy is right, you should pass the parameter..

    also another idea..
    – shouldn`t it read like that:

    public function delete($con = null)
    {
    if(parent::delete($con))
    {
    $this->getPost()->updateCommentCount(false);
    return true;
    }
    return false;
    }

    instead of

    public function delete($con = null)
    {
    $this->getPost()->updateCommentCount(false);

    return parent::delete();
    }

    same for the save() part..?

  6. ali1k Says:

    Hello,thanks for your tips. I think with a MySQL trigger we can get faster results.

  7. Idetrorce Says:

    very interesting, but I don’t agree with you
    Idetrorce

  8. zero0x Says:

    well that’s pretty cool, but what will you do in the case you don’t have to count, but for example save averages.

    For example post and rating. Rating is int 1…10, and you wanna display average ranting. You cannot use this method.

  9. cbetta Says:

    Tnx, very useful!

  10. MK Says:

    @zeroox: sure you can use this method, you just need two counters: comment_count and comment_rating_sum.

    Example:
    comment1: rating +7
    => comment_count=1, comment_rating_sum=7

    comment2: rating +3
    => comment_count=2, comment_rating_sum=7+3=10

    comment_rating_sum / comment_count is the average rating

  11. MK Says:

    quoting “evan”: “If two comments are submitted near-simultaneously the objects could each increment their count values unaware of the other comment – so your count would end up one short.”

    IMHO evan is absolutely correct (and the same can happen with “delete” of course). I just checked how the CakePHP implementation works (http://api.cakephp.org/libs_2model_2model_8php-source.html#l01257), and if I am not mistaken they count the number after each update again:

    array($assoc[‘counterCache’] => intval($this->find(‘count’, compact(‘conditions’))))

    In other words: they do an additional query after each save/delete operation. This strategy works well if the number of comments for a post has to be shown often (and takes some more time for each save/delete operation). This strategy guarantees the the cache counter is always valid.

    I also can think of eliminating the additional lookups if no collision has taken place. For this one would have to use the timestamps, i.e., only update the cache counter if the timestamp has not changed in between. If the timestamp has not changed, one could use +1/-1; if it has changed (because of a simultaneous event) a re-count has to be done.

  12. Gangster58 Says:

    You may feel more comfortable with a mental health professional who is a woman or with a support group for women. ,

  13. Daddy77 Says:

    Delta-9 tetrahydrocannabinol inhibits antitumor immunity by a CB-2 receptor-mediated, cytokine depend- ent-pathway. ,


  14. Hi there! this place is the best show case of a good wordpress design am I right? seems all so organized that makes me think its more than perfect! good luck on your next posts!


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: