Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby .each efficiency

I do (in my view):

# myUser is a User in ActiveRecord with :has_many :posts
myUser.posts.each do |post|
end

If the user had ten posts, would this be doing a database call ten times? Should these loops be like (less pretty)?:

myPosts = myUser.posts
myPosts.each do |post|
end

Here is a paste bin of a ruby file that I did to test. EDIT Modified the paste bin.

This reminds me of code in Java

for (int i = 0; i < someExpensiveFunction(); i++)

that should be (unless the array is modified)

for (int i = 0, len=someExpensiveFunction(); i < len; i++)

Am I missing something? I see a bunch of rails examples where people loop through some has_many field of an object. This is important to me as I'm trying to optimize my application's performance.

like image 400
K2xL Avatar asked Dec 04 '12 18:12

K2xL


1 Answers

If the user had 10 posts, this would technically be doing a database call 10 times?

Nope.

myUser = User.find 123
myUser.posts.each do |post|
  puts post.title
end

This code here will run 2 queries. The first will find a user by its id, returning a single row. The second will run a query asking for all posts which have a user id that matches myUser. Then the each will use the result of that to iterate through.


If you watch the log in development mode it tells you the queries it's running, and you should see a single query returning all those posts.

Rails uses an association proxy object to hold your query, execute the query when you need records, and then caches the result. It's a very helpful bit of code and, for the most part, it handles things like this for you.

This is a feature of Rails, not ruby.


At the ruby level, each simply acts on collections.

def get_letters
  puts 'executing "query"'
  sleep(3)
  ["a","b","c"]
end

get_letters.each do |item|
  puts item
end

This should print out.

executing "query"
a
b
c

The get_letters method executes, it returns an array, an that array already full of data is what we call the each method on, so we can process each item. Fetching the collection and iterating through it are 2 completely separate steps.


From your pastebin:

# will this run forever?
myArr = ["a","b","c"]
myArr.each do |item|
  myArr.push("x")
end

Yeah it will, But not because the array is being "fetched" over an over.

This equivalent to javascript like this, which spells it out better.

myArr = ["a","b","c"];
for (var i = 0; i < myArr.length; i++) {
  myArr.push('x');
}

The original array, on each iteration of the loop checks to see if it's done yet. But because the array gets longer by one every single time we progress by one item, i < myArr.length will always be true because they both increase by one on each iteration. The same thing is going on in ruby with your each loop, for the same reason.

It runs forever, not because you keep running some code to regenerate the array. It runs forever because you are artificially mutating the resulting array.

like image 135
Alex Wayne Avatar answered Sep 30 '22 12:09

Alex Wayne