25

It is my understanding that when defining a :counter_cache option it is to be specified on the model that includes the belongs_to declaration. So I am a little unsure of how to handle this when working with a has_may through association (as I believe that a belongs_to declaration is not used in this scenario):

class Physician < ActiveRecord::Base
  has_many :appointments
  has_many :patients, :through => :appointments
end

class Appointment < ActiveRecord::Base
  belongs_to :physician, :counter_cache => appointment_count
end

class Patient < ActiveRecord::Base
end

I wish to use the :counter_cache option to make finding the number of Patients belonging to a Physician more efficient.

myPhysician.patients.count

FYI: Rails 3.1

Cheers

b73
  • 1,377
  • 2
  • 14
  • 25

4 Answers4

33

I'm not sure what kind of relationship you want. That example is similar to the one in the Rails Guide

class Physician < ActiveRecord::Base
  has_many :appointments
  has_many :patients, :through => :appointments
end

class Appointment < ActiveRecord::Base
  belongs_to :physician
  belongs_to :patient
end

class Patient < ActiveRecord::Base
  has_many :appointments
  has_many :physicians, :through => :appointments
end
  • A Physician has many Appointments, and has many Patients
  • An Appoinment belongs to (has one) Physician and one Patient
  • a Patient has many Appointments and many Physicians.

Regarding the :counter_cache option, according to the belongs_to doc: If you want the number of Patients belonging to a Physician you would need:

class Appointment < ActiveRecord::Base
  belongs_to :physician, :counter_cache => :patient_count
  belongs_to :patient
end

And you need to write a migration to add the patient_count column to the Phyisicans table.

However, for has_many through relationships Rails 3.1 seems to automatically detect the counter_cache column, so you don't have to specify it (remove :counter_cache => :patient_count). If you do specify it your counter will go up by two (this is very weird).

By the way, there seems to be some problems with :counter_cache option in Rails 3.1, as reported here:

With all of that in mind, maybe your best bet is to write your own count mechanism using callbacks.

Hope it helps :)

jävi
  • 4,571
  • 1
  • 24
  • 32
  • 11
    A little update: Automatic detection of counter cache for `has_many :through` has been removed from Rails: https://github.com/sgrif/rails/commit/f072980382db8cb64c09f63001ef805843db51c8 – head May 06 '15 at 14:05
  • I don't understand how this answer got all these upvotes; this will increase the `Physician#patient_count` every time a new appointment is created, which is NOT what we want here. If a Patient has 5 appointments with the same Physician, the patient will be counted repeatedly as 5 patients, one for each appointment. – sandre89 Feb 11 '22 at 13:01
10

I added a counter_cache to a has_many :through association on Rails 5.1, and the philosophy is the same as with has_many. Using the Physician, Appointment, Patient example:

  1. add patients_count to the physicians table as an integer
  2. add a counter cache to the the join model (appointment.rb): belongs_to :physician, counter_cache: :patients_count

Note: the answer above is correct, this answer just confirms that it works on Rails 5.1.

krsyoung
  • 1,171
  • 1
  • 12
  • 24
Ivica Lakatoš
  • 301
  • 2
  • 3
1

I ran into a similar problem, counting the number of records in a two-deep relationship. In your example, this would be the number of Patients for a Physician, as opposed to the number of Appointments. (e.g. don't count multiple appointments for one patient) I haven't tested the other solutions offered, but it appears they return the number of appointments.

I found no way to do this in Rails 4, primarily because there is no belongs_to through: option. After exhausting several fruitless approaches, I found gem counter_culture. This solved the problem easily, by defining a two-deep relationship to be counted:

class Patient < ActiveRecord::Base
  belongs_to :appointment
  counter_culture [:appointment, :physician]
end

Add a counter field to Physician with:

rails generate counter_culture Physician patients_count

And voila! You can now do easy activerecord queries like:

Physician.order(patients_count: 'DESC')
David Hempy
  • 5,373
  • 2
  • 40
  • 68
0

I can also confirm that the method outlined by Ivica Lakatos works with Rails 6 for has_many :through relationships using a join model.

  1. add patients_count to the physicians table as an integer
  2. add a counter cache to the the join model (appointment.rb): belongs_to :physician, counter_cache: :patients_count
L.Youl
  • 361
  • 3
  • 9