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
end
and
class Rating < ActiveRecord::Base
belongs_to :product
end
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:
product.ratings.positive
and in the second case like this:
product.positive_ratings
This also allows you to eager load them like this:
Product.includes(:positive_ratings).
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
end
class Activity < ActiveRecord::Base
belongs_to :user, inverse_of: :activities
belongs_to :product, inverse_of: :activities
end
class View < Activity; end
class Save < Activity; end
class Product < ActiveRecord::Base
has_many :activities, inverse_of: :product
has_many :users, through: :activities
end
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'})
end
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:
User.includes(:saved_products).first
Happy preloading!