counterCache or counter_cache Behavior

by 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:

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

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


<?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));
            }
        }
    }

}
?>

Report

More on Behaviors

Advertising

Comments

  • tito posted on 01/19/10 11:33:49 AM
    How can I update the counterCache for records that ware already in database, before adding the counterCache behaviour ?
  • pixol posted on 08/30/07 06:56:33 PM
    Why not use count in mysql for the amount of comments ?
    • brightball posted on 04/16/09 04:09:46 PM
      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.
  • joelstein posted on 07/05/07 10:09:55 AM
    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!
  • bododo posted on 06/23/07 10:54:42 AM
    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. :)
  • maninderv posted on 05/02/07 11:30:59 AM
    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
  • digitalspaghetti posted on 04/30/07 06:03:57 AM
    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.
    • digitalspaghetti posted on 04/30/07 11:05:37 AM
      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
login to post a comment.