Rails Has_many Through Association With Counter Cache Argument

written in ActiveRecord, Associations, Rails

Counter cache argument prevents you from making unnecessary SELECT COUNT (*) requests.

We want to make unnecessary @course.users call for each course.

Next association is a basic example for :has_many, :through association.

courses.rb
1
2
  has_many :subscriptions
  has_many :users, through: :subsriptions
users.rb
1
2
  has_many :subscriptions
  has_many :courses, through: :subsriptions
subsription.rb
1
2
  belongs_to :user
  belongs_to :course

We must add a counter cache for course, as users_count

subsription.rb
1
2
  belongs_to :user
  belongs_to :course, counter_cache: :users_count

After that we must add a field to Course model for storing users_count

1
  rails g migration add_users_count_to_courses users_count:integer

Open the created migration file and set default value to 0 and prevent from nil records

xxx_add_users_count_to_courses.rb
1
2
3
4
5
  class AddUsersCountToCourses < ActiveRecord::Migration
    def change
      add_column :courses, :users_count, :integer, default: 0, null: false
    end
  end

After this step, @course.users will return 0, because we set the defaut value to it.

For fix this situation we must generate another migration file and fill @course.users_count field with correct values

1
  rails g migration cache_course_users_count

Open the file and fill :users_count field

xxx_add_users_count_to_courses.rb
1
2
3
4
5
6
7
8
9
10
11
  class CacheCourseUsersCount < ActiveRecord::Migration
    def up
      Course.find_each do |course|
        users_count = Subscription.where(course_id: course.id).size
        course.update_attributes(users_count: users_count)
      end
    end

    def down
    end
end

That’s all. Now you are storing users_count in database and cache it, so performance of your app increased.

Good for you!