This project is archived and is in readonly mode.

#3521 new
Scott Windsor

Add conditional counter cache

Reported by Scott Windsor | November 30th, 2009 @ 07:53 AM

One common pattern that I keep repeating is lots of repeated code to handle cache counters based on a flag or status. Take the example of Posts & Comments. I want a counter cache for comments, but I don't want to include ones that are deleted. This should also take into account updates as well as creates & deletes.

Here's an example of the syntax:

belongs_to :post, :counter_cache => {:condition => lambda {|c| !c.deleted } }

This only increments or decrements the counter if the lambda returns true (and the lambda has self passed in).

I've added support for this in the following patch. I've also included unit tests for this functionality. This works in 2-3-stable, but I haven't tested against master.

Comments and changes to this ticket

  • Les Nightingill

    Les Nightingill December 12th, 2009 @ 10:48 AM

    Not convinced that the condition pattern is as common as Scott suggests, but I definitely feel that the omission of the counter_cache increment/decrement method for updates is an oversight and should be included in Rails.

  • Scott Windsor

    Scott Windsor December 12th, 2009 @ 03:48 PM

    The conditional happens more frequently in my code because I tend to use status columns on all of my tables. That way, I only ever soft delete something (changing the status rather than actually deleting the row). But, if the status is deleted, the user likely doesn't see the object listed anymore, so the counts are off. I also always have to write multiple callbacks as well - since checking a column for changes doesn't work on create (and there's no way to tell if an object is new in after_save), I have to make two callbacks, one for after_update and one for after_create - and they usually both have very similar logic. Mostly, I'd just like to cut down on the redundant code in my models.

  • Cyrille

    Cyrille January 7th, 2010 @ 09:47 AM


    Imagine if your models have state machine most of the time you want to count only 'active' elements.
    Very common e-commerce case : product and category. In front office you might want to display very often number of "active" products per category. Make sense. Same for forum posts where posts can be soft deleted for moderation. Another great one is comments and spam you might want to display only the number of comments not marked as spam or under_moderation

  • cricy

    cricy January 26th, 2010 @ 08:35 AM

    belongs_to :post, :counter_cache => (:deleted ? 1 :0)

  • cricy

    cricy January 26th, 2010 @ 08:36 AM

    belongs_to :post, :counter_cache => (:deleted ? true : false )

  • Scott Windsor

    Scott Windsor January 26th, 2010 @ 04:41 PM

    cricy, I don't quite think that sytanx would work because it's not valid ruby. belongs_to is still a function call that takes arguments (and in the above cases a symbol, then a hash with options. Additionally, the ":deleted ? true : false" expression would be evaluated too early (at class compilation time, and not at execution time). The only real way around this is a lambda. I could simplify the code to:

    belongs_to :post, :counter_cache => lambda {|c| !c.deleted }

    But then, there's no way for the existing option of having a custom column name to be passed in W

    Which is what the current options allow:

    belongs_to :post, :counter_cache => :users_postz

    So the only way to allow both of these existing options is to provide a hash as the argument, in which case the logic can handle both options (and future options as well).

    belongs_to :post, :counter_cache => {:condition => lambda {|c| !c.deleted }, :cache_column => :user_postz}

  • Jim Meyer

    Jim Meyer August 24th, 2010 @ 12:07 AM

    Definite +1 for the functionality. The patch as offered is a good start, but will break some existing functionality. For example, it's perhaps not well known that this is a valid counter cache declaration:

    class Comment
      belongs_to :post, :counter_cache => 'visible_comments_count'

    This works because ActiveRecord::Reflection::AssociationReflection#counter_cache_column does this:

      def counter_cache_column
        if options[:counter_cache] == true
        elsif options[:counter_cache]

    ... so slinging a Hash as :counter_cache won't work. Perhaps better to pass it as :counter_cache_conditions.

    The other breakage has to do with ActiveRecord::Base.reset_counters, which currently expects to operate entirely inside the database. If the counter cache's conditions aren't able to be reduced to SQL, reset_counters must load all counted objects and consider them in Rails-space, which chews up memory and is not likely to perform well for large numbers of counted objects.

    I'm working on a fix as a plugin inside an app which needs this; if I manage it well, I'll gemify it and share.

  • Ryan Bigg

    Ryan Bigg October 9th, 2010 @ 10:11 PM

    • Tag cleared.
    • Importance changed from “” to “Low”

    Automatic cleanup of spam.

  • Jeff Kreeftmeijer

    Jeff Kreeftmeijer October 10th, 2010 @ 01:03 PM

    • Title changed from “[PATCH] Add conditional counter cache” to “Add conditional counter cache”
    • Tag set to activerecord, patch

    Using the "patch" tag instead of prefixing the ticket title with "[PATCH]" to make sure patched tickets end up in the open patches bin. :)

  • Szymon Nowak

    Szymon Nowak April 27th, 2011 @ 09:37 AM

    Wouldn't actually moving counter_cache declaration from belongs_to to has_many solve this problem?

    class Post
      has_many :likes, :class_name => "Vote", :conditions => { :flag => true }, :counter_cache => true
      has_many :dislikes, :class_name => "Vote", :conditions => { :flag => false }, :counter_cache => "some_fancy_column_name"
    class Vote
      belongs_to :post
    Post.first.likes.size # could use proper counter cache
  • swindsor

    swindsor April 27th, 2011 @ 05:18 PM

    That might be another solution - it's less backwards compatible, but might be a good direction. If you want to add a patch, feel free. Until then, I'm still waiting on someone on the rails team to actually look at this. =/

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href=""></a>