Home > database >  find_each in rails with a has_many association
find_each in rails with a has_many association

Time:07-10

I'm trying to understand the Active Record find_each method to retrieve records in batches.

If a Post has_many comments (thousands of comments) and I want to render each comment, I can do:

@post.comments.each { |comment| ... }

But is it faster if I do :

@post.comments.find_each { |comment| ... } ?

Is the second query valid?

The way I'm seeing this is that by doing @post.comments before the find_each is already getting all the comments for each post so ending the query with find_each is probably redundant ?!

This is the full block of code in a rails app views folder:

 json.comments @post.comments.find_each do |comment|
   json.rating comment.rating
   json.content comment.content
 end

Thank you

CodePudding user response:

@post.comments.find_each is correct. When you call @post.comments the query that it creates does not execute right away. That's why you can chain other query methods like where and order etc. to this association. (sql log truncated for readability)

>> comments = Post.first.comments; nil;
  Post Load (0.8ms)
=> nil

# NOTE: ^ no comments are loaded yet, until we need to output the result 

>> comments
  Comment Load (2.0ms)
=> [#<Comment:0x00007f2ef1733328 id: 1, post_id: 1>, ...]  # <= array of 2000 comments

When each method is chained, all of the comments are loaded into memory which is an issue when you have thousands of records.

>> Post.first.comments.each{|c| print "." }
  Post Load (0.6ms) 

# NOTE: executes one query for comments
  Comment Load (1.4ms)
.................................................. # <= 2000 dots
=> [#<Comment:0x00007f2ef1560be0 id: 1, post_id: 1>, ...]

When find_each method is chained, comments are retrieved in batches of 1000 by default:

>> Post.first.comments.find_each{|c| print "." }
  Post Load (0.6ms)

# NOTE: executes query to get 1000 comments at a time 

  Comment Load (0.8ms)
.................................................. # <= 1000 dots
  Comment Load (0.8ms)
.................................................. # <= 1000 dots
  Comment Load (0.4ms)
# no more comments
=> nil

You can also specify the :batch_size:

>> Post.first.comments.find_each(batch_size: 100){|c| print "." }
  Post Load (0.7ms)

# NOTE: get comments in batches of 100

  Comment Load (0.6ms)
.................................................. # <= 100 dots
  Comment Load (0.5ms)
.................................................. # <= 100 dots
# 21 queries in total
=> nil

https://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-find_each

  • Related