Last Updated: February 25, 2016
· alexvpopov

Preloading scoped has_many :through associations

Recently, I read Justin Weiss' very nice article on How to Preload Rails Scopes. The gist of it is this - if you have:

class Product < ActiveRecord::Base
  has_many :ratings


class Rating < ActiveRecord::Base
  belongs_to :product

instead of having a scope inside Rating like this:

scope :positive, -> { where 'score > 3.0'}

you can have a scoped associaton inside Product

has_many :positive_ratings, -> { where 'score > 3.0'}, class_name: 'Rating'

The rational for this is that in the first case you would call the positive ratings for a product like this:


and in the second case like this:


This also allows you to eager load them like this:


which is not possible in the first case.

An even more interesting twist: what if you have a has_many :through association between two models and the through model also happen to use the Single Table Inheritance pattern?

Let's see this in practice:

class User < ActiveRecord::Base
  has_many :activities, inverse_of: :user
  has_many :products, through: :activities

class Activity < ActiveRecord::Base
  belongs_to :user, inverse_of: :activities
  belongs_to :product, inverse_of: :activities

class View < Activity; end

class Save < Activity; end

class Product < ActiveRecord::Base
  has_many :activities, inverse_of: :product
  has_many :users, through: :activities

What if you want to get only the saved products for a user? You could add the following method to User:

def saved_products
 products.includes(:activities).where(activities: {type: 'Save'})

However, you can't preload the saved products and you would have the N+1 problem, when you load a couple of users and display their saved products. Let's fix this.

First, let's add the scoped association between User and Activity to User:

has_many :saves, -> { where type: 'Save' }, class_name: 'Activity'

This will allow us to eager load our saved products like this:

User.includes(saves: :product).first

To have it even more convenient, you need to take the next step and add the scoped association between User and Product:

has_many :saved_products, through: :saves, source: :product

The key step here is to add the source option. It:

Specifies the source association name used by hasmany :through queries. Only use it if the name cannot be inferred from the association. hasmany :subscribers, through: :subscriptions will look for either :subscribers or :subscriber on Subscription, unless a :source is given.

Since Activity does not have an association, named 'ViewedProduct', you need to specify the right name using source.

Now you can just call:


Happy preloading!