45

I've got two models.
- Parent has_many Children;
- Parent accepts_nested_attributes_for Children;

class Parent < ActiveRecord::Base
  has_many :children, :dependent => :destroy
  accepts_nested_attributes_for :children, :allow_destroy => true
  validates :children, :presence => true
end

class Child < ActiveRecord::Base
  belongs_to :parent
end

I use validation to validate presence of children for every parent, so I can't save parent without children.

parent = Parent.new :name => "Jose"
parent.save
#=> false
parent.children_attributes = [{:name => "Pedro"}, {:name => "Emmy"}]
parent.save
#=> true

validation works. Then we will destroy children via _destroy attribute:

parent.children_attributes = {"0" => {:id => 0, :_destroy => true}}
parent.save
#=> true !!!
parent.reload.children
#=> []

so I can destroy all children via nested forms and validation will pass.

Actually that happens because after I delete child from my parent via _delete, children method still returns destroyed object before I reload it, so validation passed:

parent.children_attributes = {"0" => {:id => 0, :_destroy => true}}
parent.save
#=> true !!!
parent.children
#=> #<Child id:1 ...> # It's actually deleted
parent.reload.children
#=> []

Is it bug?

What is the question. The question is best solution to repair it. My approach is to add before_destroy filter to Child to check if it is last one. But it makes system complicated.

fl00r
  • 82,987
  • 33
  • 217
  • 237

2 Answers2

64

This will probably work for you, but I have a feeling there's a much better answer out there. It sounds like a bug to me.

class Parent < ActiveRecord::Base
  validate :must_have_children

  def must_have_children
    if children.empty? || children.all?(&:marked_for_destruction?)
      errors.add(:base, 'Must have at least one child')
    end
  end
end
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
John Douthat
  • 40,711
  • 10
  • 69
  • 66
0

It's not a bug. Acording to the documentation

Validates that the specified attributes are not blank (as defined by Object#blank?)

and validates :children, :presence => true is just the the same. The documentation doesn't say what happens if you try to use it on an association. You should use custom validation using validate.

Using validates_presence_of on has_many association calls blank? on association children, which is an object of class Array. Since the blank? is not defined for an Array, it fires method_missing which is caught inside Rails. Usually it do what you wants but I found it fails in Rails 3.1rc and Ruby 1.8.7 in a really awful way: it silently reverts the changes of associated records. It took me a couple of hours to find out what's happening.

Tomáš K.
  • 11
  • 1
  • 2
    Actually the problem because it vaidates presence of children BEFORE removing children. So we should check if children are `marked_for_destruction?` – fl00r May 31 '11 at 11:06