Has_one prematurely nullifies existing association when assi


#1

Like a number of other Rails newbies, I’m seeing some unexpected
behavior with the has_one relationship. I attempted to search for a
solution or explanation here and on Rails Trac, but came up short.
I’m using Rails 1.2.2. Here are some simplified models:

class Order < ActiveRecord::Base
has_one :invoice
validates_associated :invoice
end

class Invoice < ActiveRecord::Base
belongs_to :order
validates_presence_of :description
end

The following works as expected (with output edited for brevity):

order = Order.create
=> #<Order:0x3506fb0 … @attributes={“id”=>1}>

invoice = Invoice.create(:description => “Foo”)
=> #<Invoice:0x34f24e8 … @attributes={“order_id”=>nil, “id”=>1,
“description”=>“Foo”}>

order.invoice = invoice
=> #<Invoice:0x34f24e8 … @attributes={“order_id”=>1, “id”=>1,
“description”=>“Foo”}>

Order.find(1).invoice
=> #<Invoice:0x34e7688 @attributes={“order_id”=>“1”, “id”=>“1”,
“description”=>“Foo”}>

However, when I assign an invalid Invoice to the has_one association,
the valid association is nullified in the database:

order.invoice = Invoice.new
=> #<Invoice:0x34e5810 … @errors={“description”=>[“can’t be
blank”]} … >

Order.find(1).invoice
=> nil

This behavior is surprising. Here’s another example (starting by
reassigning the valid Invoice)

order.invoice = invoice
order.build_invoice
=> #<Invoice:0x34bbd08 @new_record=true, @attributes={“order_id”=>1,
“description”=>nil}>

Order.find(1).invoice
=> nil

I realize that understanding how and when things are saved can be
confusing, especially with the has_one relationship. I’m also aware
of several ways to work around this, either by adding special code to
Order, or just assigning to the belongs_to association instead. But
can someone explain why ActiveRecord seems to be nullifying a valid
association before the new association is validated or saved?

Thanks,
Brian


#2

Anyone? Watching the development log show that ActiveRecord is
setting the order_id to null when the assignment is made.


#3

Save the invoice after associating it with the order.

-Jonathan.


#4

Hi, if the invoice is invalid, it isn’t saved to the database.
Therefore, you’re overwriting the previous value of the invoice
associated to the order. Thus, invoking find returns nil. Well, I
wish that this helps.

Good luck,

-Conrad


#5

Hi, here’s a better explaination with the relevant code that you have
posted:

  1. order.invoice = Invoice.new
    => #<Invoice:0x34e5810 … @errors={“description”=>[“can’t be
    blank”]} … >

  2. Order.find(1).invoice
    => nil

Line 1 => We’re assigning the result or ‘Invoice.new’ where all field
are initialized to
their respective default values. Also, the
validations are not invoked until
you attempt to invoke save or save! methods. Thus,
you’re assigning an
invalid invoice instance to the order.invoice

Line 2 => Now, we’re trying to find an invoice instance that results
in nil being returned
because the invoice instance in (1) was never saved
the database.

Finally, I would recommend taking a look at the relevant chapters in
AWDwRv2 for additional information.

Good luck,

-Conrad


#6

I’m not sure what you’re trying to say…yes, if I just assigned the
the order to the invoice, then saved the invoice, that would
workaround this issue. But that doesn’t answer my question. While
playing around with this, it also looks like AR is nullifying the
association in the Invoice object, as well. So, for example:

order = Order.create
=> #<Order:0x3506fb0 … @attributes={“id”=>1}>

invoice = invoice.create(:description => “Foo”)
=> #<Invoice:0x34f24e8 … @attributes={“order_id”=>nil, “id”=>1,
“description”=>“Foo”}>

order.invoice = invoice
=> #<Invoice:0x34f24e8 … @attributes={“order_id”=>1, “id”=>1,
“description”=>“Foo”}>

Order.find_first.invoice
=> #<Invoice:0x34e79f8 @attributes={“order_id”=>“1”, “id”=>“1”,
“description”=>“Foo”}>

Invoice.find_first.order
=> #<Order:0x34e2cc8 @attributes={“id”=>“1”}>

order.build_invoice
=> #<Invoice:0x34e1a58 @new_record=true, @attributes={“order_id”=>1,
“description”=>nil}>

invoice.order
=> nil

Invoice.find_first.order
=> nil

Why is the Invoice with id = 1 no longer referencing order with id =
1? All I’ve done is build a new invoice…I haven’t tried to save it,
and it’s not even valid. And I can’t just re-save the original
Invoice object, because it also no longer references the order. This
seems like a bad side-effect to me.

As I’ve said, I’m a Rails newbie, and it seems like the general wisdom
is to assign to the belongs_to side of an association, instead of the
has_one side. But it would be nice if the has_one side didn’t have
surprising side-effects. Is this a well-known bug that I just didn’t
search for long enough before I posted to this group?

Brian