counterCache or counter_cache Behavior

By Derick Ng (dericknwq)
I wrote this because I needed something simple for my recent project and CakePHP have not implemented the counterCache option yet. This is my first time writing a Behavior and largely based on http://groups.google.com/group/cake-php/msg/74508dad38d3d623 but it doesn't work for me though. You may wish to learn more about why counterCache is useful at http://groups.google.com/group/cake-php/msg/971ced72abe96b03
This is just a quick solution for me, but I thought of just sharing it so anyone could use and improve on it. There are currently no configuration for the behavior yet though because I don't need it. Feel free to bring up a new copy of the code or suggest something so I probably could improve upon mine. ;)

Usage is very simple for now and really "convention over configuration". It expects the `comments` table to have a `post_id`, and the `posts` table to have a `comments_count` column.

Model Class:

Download code <?php 
class Comment extends AppModel {
    var 
$name 'Comment';

    var 
$actsAs 'CounterCache';
    var 
$belongsTo = array('Post');
}
?>

Download code
<?php
/**
 * CounterCacheBehavior
 * 
 * @author Derick Ng aka dericknwq
 * @version 2007-05-01
 */
class CounterCacheBehavior extends ModelBehavior {
    
    var 
$foreignTableIDs = array();

    function 
setup(&$model$config = array()) {
    }

    function 
afterSave(&$model$created) {
        if (
$created) {
            foreach (
$model->belongsTo as $assocKey => $assocData) {
                
$assocModel =& $model->{$assocData['className']};
                
$field Inflector::tableize($model->name) . '_count';
                
                if (!empty(
$model->data[$model->name][$assocData['foreignKey']]) && $assocModel->hasField($field)) {
                    
$this->foreignTableIDs[$assocData['className']] = $model->data[$model->name][$assocData['foreignKey']];
                    
$count $model->findCount(array($model->name '.' $assocData['foreignKey'] => $this->foreignTableIDs[$assocData['className']]));
                    
$assocModel->id $this->foreignTableIDs[$assocData['className']];
                    
$assocModel->save(array($field => $count), false, array($field));
                }
            }
        }
    }

    function 
beforeDelete(&$model) {
        foreach (
$model->belongsTo as $assocKey => $assocData) {
            
$this->foreignTableIDs[$assocData['className']] = $model->field($assocData['foreignKey']);
        }
        return 
true;
    }

    function 
afterDelete(&$model) {
        foreach (
$model->belongsTo as $assocKey => $assocData) {
            
$assocModel =& $model->{$assocData['className']};
            
$field Inflector::tableize($model->name) . '_count';
            
            if (
$assocModel->hasField($field)) {
                
$count $model->findCount(array($model->name '.' $assocData['foreignKey'] => $this->foreignTableIDs[$assocData['className']]));
                
$assocModel->id $this->foreignTableIDs[$assocData['className']];
                
$assocModel->save(array($field => $count), false, array($field));
            }
        }
    }

}
?>

 

Comments 354

CakePHP Team Comments Author Comments
 

Comment

1 Excellent bit of code

Very nice! This has allowed me to remove some really ugly code from my Post/Comment system, and I love that I can now use it on any model (such as User/Posts counts).

This should definitely be added to the core functionality.
Posted Apr 30, 2007 by Tane Piper
 

Comment

2 although I have hit an issue....

Ok, I seem to have come across one issue that I cannot seem to solve, so if you can help....!!!

The 3 models that I am having an issue with are my Comment, User and Post models. When a user add's a comment to a post, I want to increase the comments_count on both Post and User. In my standard set up, the Post model comments_count does increase, but the User one throws out an SQL error:


Query: SELECT COUNT(*) AS count FROM `comments` AS `Comment` LEFT JOIN `posts` AS `Post` ON (`Comment`.`post_id` = `Post`.`id`) LEFT JOIN `users` AS `User` ON (`Comment`.`user_id` = `User`.`id`) WHERE `user_id` = 1

Warning (512): SQL Error: 1052: Column 'user_id' in where clause is ambiguous [CORE/cake/libs/model/datasources/dbo_source.php, line 479]

From what I can tell, it's not trying to increase the users overall comments_count, it's trying to do it in relation to the post_id. If I unbind the models, and only use the User model, it works fine. I've also tried reducing the recursion, but again no joy with it.

For example, if I unbind to use just the User model the SQL looks like this:

SELECT COUNT(*) AS count FROM `comments` AS `Comment` LEFT JOIN `users` AS `User` ON (`Comment`.`user_id` = `User`.`id`) WHERE `user_id` = 1
Posted Apr 30, 2007 by Tane Piper
 

Comment

3 try this

I don't know if it will help you for sure but the ambiguos error comes when the same field exists in 2 tables and sql cant figure out what you are referring to.

Try:

Query: SELECT COUNT(*) AS count FROM `comments` AS `Comment` LEFT JOIN `posts` AS `Post` ON (`Comment`.`post_id` = `Post`.`id`) LEFT JOIN `users` AS `User` ON (`Comment`.`user_id` = `User`.`id`) WHERE `User.user_id` = 1
Posted May 2, 2007 by Mandy Singh
 

Comment

4 table names and queries

Hi guys :)

maybe it's just me, but why not set these conventions:

1) if A belongs to B then B should have a field named A_count, ok that's already done.

2) whenever we save or delete, we have one query per associated model, using the model->query() method.

3) We don't need to know the value of a field to increment/decrement it, since
we can write something like:

$model->query("UPDATE $model->table SET comments_count=comments_count+1 WHERE post_id='$id')

See what I mean? that's only one query!

What do you think?

P.S. Ok, okay... I hear you say: "don't talk, code!"
Promise, i'll try to bring my vision of things in a new behavior. :)
Posted Jun 23, 2007 by bododo
 

Comment

5 One suggestion...

This is a great behavior; thanks so much for putting it together. However, I have a suggestion of how you can make it better.

Since a model can belong to another model with a specified foreignKey and conditions, you should also incorporate any conditions when doing a the findCount calls. Here's what I suggest you change in afterSave and afterDelete, so that any declared conditions in the model association as used. Change...


$count = $model->findCount(array($model->name . '.' . $assocData['foreignKey'] => $this->foreignTableIDs[$assocData['className']]));

to...


$conditions = am(array($model->name . '.' . $assocData['foreignKey'] => $this->foreignTableIDs[$assocData['className']]), $assocData['conditions']);
$count = $model->findCount($conditions);

Otherwise, congrats on the great behavior!
Posted Jul 5, 2007 by Joel Stein
 

Comment

6 count

Why not use count in mysql for the amount of comments ?
Posted Aug 30, 2007 by chris
 

Comment

7 Performance

Why not use count in mysql for the amount of comments ?
That count isn't going to change so using counterCache cuts out a lot of reporting overhead. Say for example, that you're wanting to get some information regarding totals in about 6 different tables and then you only want to see the results that have numbers within a certain range.

Without the counterCache, you have to run multiple count() queries using joins and HAVING clauses on the results of the counts...every time you want to see that information. If you just have the counterCache, the information is already there.
Posted Apr 16, 2009 by Barry
 

Comment

8 Update current records

How can I update the counterCache for records that ware already in database, before adding the counterCache behaviour ?
Posted Jan 19, 2010 by Sacha