Unequal Object Identities in Rails


#1

My question is best illustrated by an example. Let’s say we have two
simple classes, Order and LineItem defined as follows.

class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end

We then open up script/console and load our first order and compare the
object IDs of the order that is returned and the order on the first line
item. These values never seem to be the same no matter how I obtain the
line item.

o = Order.find(:first)
l = o.line_items.first
o.object_id == l.order.object_id
=> false

The IDs are also not the same if you do this…

o = Order.find(:first, :include => :line_items)
l = o.line_items.first
o.object_id == l.order.object_id
=> false

Or this…

o = Order.find(:first, :include => :line_items)
l = o.line_items.select{ |l| true }.first
o.object_id == l.order.object_id
=> false

I noticed this because I have a method in my LineItem class that
attempts to update a value on it’s parent Order object like so:

def line_total=(value)
self.line_total = value
self.order.total += self.line_total
end

The parent Order’s value is never updated because it is a different
object instance than the Order that the line item has a reference to.
Am I doing something wrong or is this always the case in Rails? Any
help would be much appreciated.

Regards,
Jason


#2

On 8/2/07, Jason F. removed_email_address@domain.invalid wrote:

The IDs are also not the same if you do this…

o.object_id == l.order.object_id
The parent Order’s value is never updated because it is a different
object instance than the Order that the line item has a reference to.
Am I doing something wrong or is this always the case in Rails? Any
help would be much appreciated.

Regards,
Jason

They’re different ruby objects. But you should be able to do: @order
== @line_item.order, since that checks the #id of both records.

In your LineItem class, you’ll have to save the order to the database
and reload any instances you have in memory to see the changes
reflected.


Rick O.
http://lighthouseapp.com
http://weblog.techno-weenie.net
http://mephistoblog.com


#3

Jason F. wrote:

… the line item would have a reference to the same (Ruby) order
object in memory since I’m including the line items in the load AND
using an Array method to select the object. It seems wasteful to have
the line item keep it’s own copy of the order object in memory and makes
it difficult to encapsulate functionality cleanly.

I just played around with this. It’s even worse than that.

After these two lines

o = Order.find(:first, :include => :line_items)
l = o.line_items.select{ |l| true }.first

the l object does not have any order attribute at all. In the third
line,

o.object_id == l.order.object_id

l.order actually loads a new order object with a new SQL query.

Seems wasteful to me too.


#4

Rick O. wrote:

In your LineItem class, you’ll have to save the order to the database
and reload any instances you have in memory to see the changes
reflected.

Any idea why it is implemented this way? At the very least I would
think that under this scenario…

o = Order.find(:first, :include => :line_items)
l = o.line_items.select{ |l| true }.first
o.object_id == l.order.object_id
=> false

… the line item would have a reference to the same (Ruby) order
object in memory since I’m including the line items in the load AND
using an Array method to select the object. It seems wasteful to have
the line item keep it’s own copy of the order object in memory and makes
it difficult to encapsulate functionality cleanly.

I don’t mean to shoot the messenger with this post. I’m just wondering
if you (or anyone else) could shed some light on the implementation
motivations here.

Thanks for the quick reply.

Regards,
Jason


#5

Seems wasteful to me too.

Proper ORM identity systems are tough to get right, and just not a big
deal 90% of the time. Consider this situation:

o1 = Order.find 1
o2 = Order.find 1, :include => :line_items

Two “equal” orders, but very different because one has the line_items
pre-ordered.

In your case you can do this:

o = Order.find(:first, :include => {:line_items => :order})

But, that generates an SQL query of WTF proportions.

I wrote about this in more depth on my blog, going into the query
caching stuff in Edge Rails, and an active_record_context plugin I
wrote. They’re both very simple implementations of an identity system
that help out in most of the common cases:

http://activereload.net/2007/5/23/spend-less-time-in-the-database-and-more-time-outdoors


Rick O.
http://lighthouseapp.com
http://weblog.techno-weenie.net
http://mephistoblog.com