has_one prematurely nullifies existing association when assigned an invalid object

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

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

Save the invoice after associating it with the order.

-Jonathan.

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

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

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