Relations with find_each

I’m a bit unclear what’s going on with relations. I’m hoping someone can help explain it to me.

Lets say we have a model with orders and line_items such that an order has many line_items. Thus:

class Order

has_may :line_items

end

class LineItem

belongs_to :order

end

When I have an order and call line_items what’s going on? e.g.

Order order = Order.first

order.line_items.each {|li| puts li }

I thought that was basically an alias for:

Order order = Order.first

LineItem.where(:order_id => order.id).each {|li| puts li }

but, this isn’t consistent with batching.

So if we do

Order order = Order.first

this doesn’t appear to work, it’s not batching, just finding all of the line items and iterating

order.line_items.find_each {|li| puts li }

this seems to work as expected (i.e. it batches the retrieval from the db)

LineItem.where(:order_id => order.id).find_each {|li| puts li }

Can anyone explain to me why these to are different?

John H. wrote in post #1063205:

I'm a bit unclear what's going on with relations. I'm hoping someone can help explain it to me.

Lets say we have a model with orders and line_items such that an order has many line_items. Thus:

class Order   has_may :line_items end class LineItem   belongs_to :order end

When I have an order and call line_items what's going on? e.g.

Order order = Order.first order.line_items.each {|li| puts li }

I thought that was basically an alias for:

Order order = Order.first LineItem.where(:order_id => order.id).each {|li| puts li }

This is an over-simplification. Let me illustrate by example:

1.9.3p194 :010 > order = Order.first   Order Load (0.3ms) SELECT "orders".* FROM "orders" LIMIT 1 => #<Order id: 1, name: "First item", description: "First one", price: nil, created_at: "2012-06-06 01:20:03", updated_at: "2012-06-06 01:20:03">

Notice first that selecting the order does not touch the line_items table at all. At this point the order.line_items method is represented by an ActiveRecord::Relation object. Accessing this relation object will load (fire) the relation and populate the array.

Now let's take look at the line_items:

1.9.3p194 :002 > order.line_items   LineItem Load (0.2ms) SELECT "line_items".* FROM "line_items" WHERE "line_items"."order_id" = 1 => [#<LineItem id: 1, order_id: 1, name: "Line 1", quantity: 5, created_at: "2012-06-06 01:21:24", updated_at: "2012-06-06 01:21:24">] 1.9.3p194 :003 > order.line_items => [#<LineItem id: 1, order_id: 1, name: "Line 1", quantity: 5, created_at: "2012-06-06 01:21:24", updated_at: "2012-06-06 01:21:24">]

Notice the first time we access line_items ActiveRecord will load the association by running a generated SQL statement. Subsequent calls do not reissue the SQL statement.

Let's take a look at what is reported as the class for order.line_items:

1.9.3p194 :003 > puts order.line_items.class Array

This appears to be a typical Array object, but it's really more complicated than that. What you're seeing is not a simple Array object, but rather an Array that has been extended with some Rails magic. Let's take a look that the ancestry of that resulting "Array" object:

1.9.3p194 :004 > puts order.line_items.class.ancestors Array JSON::Ext::Generator::GeneratorMethods::Array Enumerable Object PP::ObjectMixin JSON::Ext::Generator::GeneratorMethods::Object ActiveSupport::Dependencies::Loadable Kernel BasicObject

Here's the ancestry of a plain old Ruby Array:

# Ruby (No Rails) $ irb 1.9.3p194 :001 > Array.ancestors => [Array, Enumerable, Object, Kernel, BasicObject]

In fact all Array objects in rails have the additional magic added to them:

# Ruby on Rails $ rails console Loading development environment (Rails 3.2.3) 1.9.3p194 :001 > puts Array.ancestors Array JSON::Ext::Generator::GeneratorMethods::Array Enumerable Object PP::ObjectMixin JSON::Ext::Generator::GeneratorMethods::Object ActiveSupport::Dependencies::Loadable Kernel BasicObject

but, this isn't consistent with batching.

So if we do

Order order = Order.first

# this doesn't appear to work, it's not batching, just finding all of the line items and iterating order.line_items.find_each {|li| puts li }

Hopefully what I explained above will clear up what's happening here. That very first access of order.line_items is causing that relation object to fire and load the objects.

What you probably want is the find_in_batches method provided by ActiveRecord:

Thanks.

(the find_each I referenced in my question is (basically) the same as find_in_batches)

The question is really why:

order.line_items != LineItem.where(:order_id => order.id)

So, I think what you’re saying is that

order.line_items returns an array, not a relation.

order.line_items != LineItem.where(:order_id => order.id)

Let me rephrase Robert's words. The key difference here is, that LineItem.where(:order_id => order.id) doesn't fire the sql and returns ActiveRecord::Relation object. While the order.line_items runs SQL and returns the Array of records. The purpose of it is that u can change the resulting SQL and do the method chaining.

Try next: line_items_by_order_id = LineItem.where(:order_id => order.id) # => object of ActiveRecord::Relation class ordered_line_items_by_order_id = LineItem.where(:order_id => order.id).order(:updated_at) # => object of ActiveRecord::Relation class ordered_line_items_by_order_id_with_positive_price = LineItem.where(:order_id => order.id).order(:updated_at).where("line_items.price > ?", 20) # => object of ActiveRecord::Relation class # ... and so on # where order.line_items # returns [LineItem, LineItem ...] array

It is noteworthy that one can still do order.line_items.where( some condition ) even though order.line_items appears to be an Array.

Colin

I think that’s what’s always tripped me up. Most ActiveRecord commands still work normally on the array. I’ve always chained them and thought I was building a relation, not getting the array. It wasn’t until I had a big set of records and discovered I couldn’t use the association in a find_each that I tripped over this.

Thanks for the help.