Rails provide different ways to load associations from the database to avoid N + 1 query being fired. In this post, I will show you how to include associations with scope applied on them.

I have two models User and Post with following associations:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
  scope :published, -> { where(published: true) }
end

Now if you loop over the association you will see N + 1 queries being fired

User.all.map do |user|
  [user.name, user.posts.map(&:title).join(', ')]
end
User Load (0.3ms)  SELECT "users".* FROM "users"
Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 3]]
#=> [["Jack", "post-1, post-2"], ["Adam", "post-3, post-3"], ["John", "post-5, post-6"]]

Now let’s try including posts

User.includes(:posts).map do |user|
  [user.name, user.posts.map(&:title).join(', ')]
end
User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
#=> [["Jack", "post-1, post-2"], ["Adam", "post-3, post-3"], ["John", "post-5, post-6"]]

Well it worked. But what if I want to apply published scope on posts.

User.includes(:posts).map do |user|
  [user.name, user.posts.published.map(&:title).join(', ')]
end
User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? AND "posts"."published" = 't'  [["user_id", 1]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? AND "posts"."published" = 't'  [["user_id", 2]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? AND "posts"."published" = 't'  [["user_id", 3]]
#=> [["Jack", "post-1, post-2"], ["Adam", "post-3, post-3"], ["John", "post-5, post-6"]]

It fired N + 1 queries. Let’s fix this by creating a new association with a condition.

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
  scope :published, -> { where(published: true) }
end

Now we will include published_posts instead posts and it will load posts with published scope applied to them.

User.includes(:published_posts).map do |user|
  [user.name, user.published_posts.map(&:title).join(', ')]
end
User Load (0.3ms)  SELECT "users".* FROM "users"
Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE (published = 't') AND "posts"."user_id" IN (1, 2, 3)
#=> [["Jack", "post-1, post-2"], ["Adam", "post-3, post-3"], ["John", "post-5, post-6"]]

You can use scope instead of condition so that you don’t have to change it twice in case you change the scope.

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
  has_many :published_posts, -> { published }, class_name: 'Post'
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
  scope :published, -> { where(published: true) }
end

Voila!