Benito Serna
Ruby/Rails, TDD, Software...

Association with default order, to fix the latest-comment n+1 problem

Do you have a method that works fine with a single record, but when you use it on a list causes N+1 queries?

Maybe, you are building a rails app and you want to fetch a list of posts with the latest comment (or something similar)…

You have a model like this…

class Posts
  has_many :comments

  def latest_comment
    comments.order(:created_at).last
  end
end

When you are fetching a single post there is no problem…

post = Post.find(id)
puts post.latest_comment

But when you try to fetch a list of posts, the method seems to ignore the includes and runs a query for each post to get the latest comment for each post =(.

Post.includes(:comments).each do |post|
  puts post.latest_comment
end

You can write a different method to get the latest comment with ruby (and not ActiveRecord), but it feels weird, and you know that it can bring you troubles later =(.

def self.latest_comment(post)
  post.comments.sort_by(&:created_at).last
end

Is this the only solution?

Well, nop… there are many ways to solve this =)

And although ther is no perfect solution, I want to tell you the one that I think that you should try first…

A default order for the association

On the definition of your association, you will pass a scope with the default order. It looks like this…

class Post
  has_many :comments, -> { order(:created_at) }

  def latest_comment
    comments.last
  end
end
Post.includes(:comments).each do |post|
  puts post.latest_comment
end

If you are not sure about the default order in the association, you can create a new association…

class Post
  has_many :comments
  has_many :sorted_comments, -> { order(:created_at) }, class_name: Comment

  def latest_comment
    sorted_comments.last
  end
end
Post.includes(:sorted_comments).each do |post|
  puts post.latest_comment
end

If you want to know more about how this part of the has_many association work, you can check with the “Scopes” section in the docs.

I hope this tip, can help you =)

Note: As as I said, this solution is not perfect, it can cause some performance problems if you have many comments per posts, because you are preloading all the coments for the list of posts, but don’t worry for now unless you will have many (maybe thousands) of comments per post =)