Yii 1.1: Using counters with ActiveRecord

19 followers

Let's say, for example, that you are developing a blog or some kind of CMS and you want to track the number of times each post was viewed (maybe to show a list of the most viewed ones).

The easier way to do that is by adding a column to the post table, which will be used to store the number of visits of that item. Each time the post is displayed the value of this column will be increased by 1. The code for this would be something like:

public function actionView($id) {
    $post = Post::model()->findByPk($id);
    $post->visits += 1;
    $post->save();
    $this->render('view', array('post' => $post));
}

We have three problems with this approach. All we want is to update the visits column, but the entire record will be updated. Also, if we didn't pass the false argument to the save method, all the validation process will be executed. Also if someone in parallel will update record from 4 to 5, we still save it as 5, as our version of post record have 4.

Since 1.1.8, the CActiveRecord class has a method that can help us with this task. It's the CActiveRecord::saveCounters() method and its usage is pretty simple:

public function actionView($id) {
    $post = Post::model()->findByPk($id);
    $post->saveCounters(array('visits'=>1));
    $this->render('view', array('post' => $post));
}

With this, only the visits column will be updated and the validation will not be triggered.

If you're not using the 1.1.8 yet, there's the CActiveRecord::updateCounters() method:

public function actionView($id) {
    $post = Post::model()->findByPk($id);
    $post->updateCounters(
        array('visits'=>1),
        array('condition' => "id = :id"),
        array(':id' => $this->id),
    );
    $this->render('view', array('post' => $post));
}

The piece of code above does exactly the same that saveCounters does. Note that updateCounters has two additional parameters. After the column used to count, we pass a condition to tell which record(s) should be updated. If you used any paramater in this condition (like the ":id"), you should pass the parameter value in the third argument of the method.

Important: If you do not pass a condition, all records will be updated.

Total 8 comments

#9040 report it
Temir at 2012/07/16 02:03am
this way it worked

the updateCounters way worked only when I changed array('condition' => "city_id = :id") to just "city_id = :id" :

$post->updateCounters(
array('product_count'=>-1), 
"city_id =:id", 
array(':id' =>1)
);
#5990 report it
davi_alexandre at 2011/12/03 08:43am
@lubosdz

I also don't like to encourage developers to cancel validation. That's why I noted that it can be done but didn't used it in my example. I'd prefer to use the save method only when I need to insert or update an entire record. I don't like to think that the same method can be used to update an entire record or just some fields. That's too many responsibilities to one single method.

Besides that, calling the save method will trigger the beforeSave and afterSave events, which I think isn't necessary in this scenario. Instead of your approach, I think that saveAttributes is a better choice.

Basically, the difference between saveAttributes and saveCounters is that with the later we don't have to manually increment the field value. Also, thinking about clean code, the saveCounters method name is clearly indicating that we're dealing with counters.

#5987 report it
lubosdz at 2011/12/02 03:30pm
Inaccurate information

Hi,

I think the first example is inaccurate because you can easily update only a single column by passing array of columns to be updated like so:

$post->save(true, array('visits'));

If you dont want do any validation use:

$post->save(false, array('visits'));

But normally validation should be encouraged.

#5985 report it
redguy at 2011/12/02 09:10am
@davi_alexandre

@davi_alexandre: now, when you updated your article it makes more sense :) My comment was made when there was no call to render method in this action and Post instance was not used in any way...

#5983 report it
davi_alexandre at 2011/12/02 09:09am
what about manipulation

@dionyseos the purpose of the article is to show the usage of the saveCounters and updateCounters methods. Check if the user refreshed the page or something like that is out of the scope of this article

#5982 report it
davi_alexandre at 2011/12/02 09:01am
Static call

@redguy you're right. You don't need to instantiate the ActiveRecord to use the updateCounters method, but my examples are all based in an action that loads a post, increment the counter and then display the loaded item (just updated the article to make this clear).

#5981 report it
dionyseos at 2011/12/02 07:46am
what about "manipulation"

with this mehtod you will update your counter every time when a view is called, but what i.e. about users who refresh their browser? this piece of code as a standalone will not produce a really usefull statistic. you have to think about using cookie or session to avoid counting something like refreshing the browser.

#5977 report it
redguy at 2011/12/02 02:10am
static call

Second example ia a little bit confusing - you do not need to fetch object by PK prior to updateCounters call (in fact it will be only extra database query that is not needed at all):

public function actionView($id) {
    Post::model()->updateCounters(
        array('visits'=>1),
        array('condition' => "id = :id"),
        array(':id' => $id),
    );
}

Leave a comment

Please to leave your comment.

Write new article