TL;DR: child.valid? == false parent.save #=> true child.new_record? #=> true child is not saved, but parent is saved I had expected this to block saving of parent
Hi,
I am confused by this behavior (ruby 1.9.3 Rails 3.2.0):
class Parent < ActiveRecord::Base
has_one :child
end
class Child < ActiveRecord::Base validates :name, :presence => true end
$ rails c Loading development environment (Rails 3.2.0) 1.9.3-p0 :001 > p = Parent.new(:name => “dad”)
=> #<Parent id: nil, name: “dad”, created_at: nil, updated_at: nil>
1.9.3-p0 :002 > p.child = Child.new(:name => “Sarah”) (0.2ms) BEGIN (0.2ms) COMMIT => #<Child id: nil, name: “Sarah”, parent_id: nil, created_at: nil, updated_at: nil>
1.9.3-p0 :003 > p.save! (0.2ms) BEGIN SQL (4.8ms) INSERT INTO “parents” (“created_at”, “name”, “updated_at”) VALUES ($1, $2, $3) RETURNING “id” [[“created_at”, Tue, 24 Jan 2012 10:06:59 UTC +00:00], [“name”, “dad”], [“updated_at”, Tue, 24 Jan 2012 10:06:59 UTC +00:00]]
SQL (0.8ms) INSERT INTO “children” (“created_at”, “name”, “parent_id”, “updated_at”) VALUES ($1, $2, $3, $4) RETURNING “id” [[“created_at”, Tue, 24 Jan 2012 10:06:59 UTC +00:00], [“name”, “Sarah”], [“parent_id”, 2], [“updated_at”, Tue, 24 Jan 2012 10:06:59 UTC +00:00]]
(9.8ms) COMMIT => true
both are saved as expected (with child “auto-saved”)
1.9.3-p0 :004 > m = Parent.new(:name => “mom”) => #<Parent id: nil, name: “mom”, created_at: nil, updated_at: nil>
1.9.3-p0 :005 > m.child = Child.new(:name => nil) # EMPTY NAME (0.2ms) BEGIN (0.2ms) COMMIT => #<Child id: nil, name: nil, parent_id: nil, created_at: nil, updated_at: nil> 1.9.3-p0 :006 > m.valid?
=> true 1.9.3-p0 :007 > m.child.valid? => false
child is not valid (name is not present)
1.9.3-p0 :008 > m.save! (0.2ms) BEGIN SQL (0.5ms) INSERT INTO “parents” (“created_at”, “name”, “updated_at”) VALUES ($1, $2, $3) RETURNING “id” [[“created_at”, Tue, 24 Jan 2012 10:07:42 UTC +00:00], [“name”, “mom”], [“updated_at”, Tue, 24 Jan 2012 10:07:42 UTC +00:00]]
(12.3ms) COMMIT => true
the save of the parent happily continues and the child is silently not auto-saved.
I had expected that the entire save! would have failed in a transaction, so that either ALL or NOTHING are saved.
When I add to the model e.g. the :autosave => true
option, I get the expected behavior:
class Parent < ActiveRecord::Base
has_one :child, :autosave => true
end
class Child < ActiveRecord::Base validates :name, :presence => true end
$ rails c
Loading development environment (Rails 3.2.0)
1.9.3-p0 :001 > # with :autosave => true on the has_one :child
association
1.9.3-p0 :002 > m = Parent.new(:name => “mom”) => #<Parent id: nil, name: “mom”, created_at: nil, updated_at: nil> 1.9.3-p0 :003 > m.valid? => true 1.9.3-p0 :004 > m.child = Child.new(:name => nil)
(0.1ms) BEGIN (0.1ms) COMMIT => #<Child id: nil, name: nil, parent_id: nil, created_at: nil, updated_at: nil> 1.9.3-p0 :005 > m.valid? => false
it seems :autosave => true
also implies validates_associated
on the association ?
1.9.3-p0 :006 > m.child.valid? => false 1.9.3-p0 :007 > m.save! (0.2ms) BEGIN (0.2ms) ROLLBACK ActiveRecord::RecordInvalid: Validation failed: Child name can’t be blank …
Next to :autosave => true
, also using validates_associated :child
or
accepts_nested_attributes_for
all result in the behavior I had expected
(save does “all or nothing”).
But, I would expect the standard functionality (without :autosave => true or
validates_associated) to not save anything (neither parent or children) in the transaction when one of the objects for saving is invalid.
I feel the current behavior allows a “silent” failure where only half of the expected
objects is saved while the save(!) returns success.
I am not pleading to make :autosave => true
or validates_associated
the
default on all associations.
I am pleading for the “ad-hoc” measure that
- if ActiveRecord decides to auto-save associated objects together with the main object
- and one of thos auot-saves fails on any of those associated objects
- then the entire transaction is rolled back and a non-success result is returned
If there is interest in this, I may look in the code and try to find the place to fix it, but maybe there are fundamental reasons for the way it works today.
Thanks for your time,
Peter