Search code examples
sqlruby-on-railsrubycounter-cache

Avoiding individual database calls for count


My models look like this:

class Movie < ActiveRecord::Base
  attr_accessible :title, :year, :rotten_id, :audience_score,
    :critics_score, :runtime, :synopsis, :link, :image

  has_many :jobs, :dependent => :destroy
  has_many :actors, :through => :jobs
end

class Actor < ActiveRecord::Base
  attr_accessible :name
  has_many :movies, :through => :jobs
  has_many :jobs, :dependent => :destroy
end

class Job < ActiveRecord::Base
  attr_accessible :movie_id, :actor_id

  belongs_to :movie
  belongs_to :actor
end

When I'm displaying my index of Actors, I'd like to show the number of movies each actor has starred in. I can do this with @actor.movies.count, however this generates an SQL query for each actor. With, say, 30 actors, this will result in 30 extra queries in addition to the initial.

Is there any way to include the count of movies each actor has participated in, in the initial Actor.all call? And thereby getting things done with only one call. Extra bonus if this was sorted by said count.

Update: All answers provided has been helpful, and though it turned into some dirt-slinging-contest at some point, it worked out well. I did a mish-mash of all your suggestions. I added a movies_counter column to my Actor model. In my Job model I added belongs_to :actor, :counter_cache => :movies_counter. This works brilliantly, and is automatically updated when i create or destroy a movie, without me adding any further code.


Solution

  • As @Sam noticed, you should add new column to actors table movies_counter

    rails g migration add_movies_counter_to_actor movies_counter:integer
    

    Now you can edit your migration

    class AddMoviesCounterToActor < ActiveRecord::Migration
      def self.up
        add_column :actors, :movies_counter, :integer, :default => 0
    
        Actor.reset_column_information
        Actor.all.each do |a|
          a.update_attribute :movies_counter, a.movies.count
        end
      end
    
      def self.down
        remove_column :actors, :movies_counter
      end
    end
    

    And run it

    rake db:migrate
    

    Then you should add two callbacks: after_save and after_destroy

    class Movie < ActiveRecord::Base
      attr_accessible :title, :year, :rotten_id, :audience_score,
        :critics_score, :runtime, :synopsis, :link, :image
    
      has_many :jobs, :dependent => :destroy
      has_many :actors, :through => :jobs
    
      after_save :update_movie_counter
      after_destroy :update_movie_counter
    
      private
      def update_movie_counter
        self.actors.each do |actor|
          actor.update_attribute(:movie_count, actor.movies.count)
        end
      end
    end
    

    Then you can call some_actor.movies_counter