validates_presence_of when using nested_attributes #1383

Closed
tom-kuca opened this Issue May 28, 2011 · 2 comments

Comments

Projects
None yet
2 participants
@tom-kuca

I found a situation in which the record is invalid even if it should be valid.

Example

Let's have a zoo with animals. An animal can be i.e. Lion (we are using polymorphic association here). Also let's assume that each animal must be in zoo, and validate it. In terms of models:

class Zoo < ActiveRecord::Base
  has_many :animals
  has_many :lions, :through => :animals, :source => :sort, :source_type => "Lion" 

  accepts_nested_attributes_for :lions
end


class Animal < ActiveRecord::Base
  belongs_to :zoo
  belongs_to :sort, :polymorphic => true

  validates_presence_of :zoo
end


class Lion < ActiveRecord::Base
  has_one :animal, :as => :sort
  has_one :zoo, :through => :animal

  accepts_nested_attributes_for :animal
end

We may want to create Zoo using nested_attributes:

Zoo.create!(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])

It fails with an exception:

ActiveRecord::RecordInvalid: Validation failed: Lions animal zoo can't be blank

I expect the zoo is 'magically' assigned to animal and the record is created.

More details

If the validates_presence_of is ommited, the record is saved without an error and the zoo is assigned, following test passes:

zoo = Zoo.create!(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])
assert_equal(zoo.animals.first.sort.name, "Simba")
assert_equal(zoo.animals.first.sort.zoo, zoo)

The problem occurs only if we pass the hash :animal_attributes. If we ommit it, the record is saved correctly.

Zoo.create!(:lions_attributes => [{:name => "Simba}])

The problem is not connected with polymorphic association, it fails in the same way if we replace the polymorphic association with a belongs to association. However I use the polymorphic one in the example, so that the schema makes sense.

In my application the problem is even worse - it sometimes saves the record, sometimes it fails - the same code behave different among the tests. If I create the models using the code from first example, it just fails.

Tested on:

  • Rails 3.1rc
  • Ruby 1.8.7, 1.9.2p180

I believe the bug is not specific to the latest version of rails since I mess with it since Rails 2.3.x, but I didn't test it.

@jonsgreen

This comment has been minimized.

Show comment Hide comment
@jonsgreen

jonsgreen Jun 1, 2011

Have you tried using the :inverse_of option on the associations. I believe that it solves this issue.

http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods#971-Validating-presence-of-parent-in-child

Have you tried using the :inverse_of option on the associations. I believe that it solves this issue.

http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods#971-Validating-presence-of-parent-in-child

@tom-kuca

This comment has been minimized.

Show comment Hide comment
@tom-kuca

tom-kuca Jun 1, 2011

It shows I haven't enough. According to the documentation:

# There are limitations to <tt>:inverse_of</tt> support:
#
# * does not work with <tt>:through</tt> associations.
# * does not work with <tt>:polymorphic</tt> associations.
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.

So this issue may be closed with the resume that the example is not supposed to work because of limitations of inverse_of. But a warning or an exception that says "do not do this" would be nice. If you close it you can stop reading here, the code below is trying to get it work using inverse_of on polymorphic association and since it it undefined it can (and does) work strangly.

I play with it more and I found a solution which works for polymorphic association at first glance, but there are a few gotchas. It looks like this (I know it's not ideal but it's the only one which at least create associated record):

class Zoo < ActiveRecord::Base
  has_many :animals, :inverse_of => :zoo
  has_many :lions, :through => :animals, :source => :sort, :source_type => "Lion"

  accepts_nested_attributes_for :lions
end

class Animal < ActiveRecord::Base
  belongs_to :zoo
  belongs_to :sort, :polymorphic => true, :inverse_of => :animal, :class_name => "Lion"

  validates_presence_of :zoo
end

class Lion ## the same as before

The only associations for which inverse_of option must be specified are Zoo->animals and the polymorphic one Animal->sort. The others seems that they don't modify the behaviour. There problems now are following:

Problem 1

Even if the record is created, the attributes are not correctly assigned (:mammal should be true):

zoo = Zoo.create(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])
puts zoo.animals.inspect             # [#<Animal id: 1, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">]
puts zoo.animals.map(&:sort).inspect # [#<Lion id: 1, name: "Simba">]

Problem 2

The class is specified for polymorphic association and if we have also i.e. Tiger, we can't use it. I have tried to solve it by creating a new polymorphic association specific for a lion:

class Animal < ActiveRecord::Base
  belongs_to :zoo
  belongs_to :sort, :polymorphic => true, :inverse_of => :animal, :class_name => "Lion"
  belongs_to :sort_lion, :polymorphic => true, :foreign_key => :sort_id, :foreign_type => :sort_type, :inverse_of => :animal, :class_name => "Lion"

  validates_presence_of :zoo
end

class Zoo < ActiveRecord::Base
  has_many :animals, :inverse_of => :zoo
  has_many :lions, :through => :animals, :source => :sort_lion, :source_type => "Lion"

  accepts_nested_attributes_for :lions
end

class Lion ## the same as before

Now it fails in another way, it creates two records instead of one:

zoo = Zoo.create(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])

zoo.animals.inspect
# [#<Animal id: 1, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">, #<Animal id: 2, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">]

zoo.animals.map(&:sort).inspect
# [#<Lion id: 1, name: "Simba">, #<Lion id: 1, name: "Simba">]

This could be connected with issue #1358

tom-kuca commented Jun 1, 2011

It shows I haven't enough. According to the documentation:

# There are limitations to <tt>:inverse_of</tt> support:
#
# * does not work with <tt>:through</tt> associations.
# * does not work with <tt>:polymorphic</tt> associations.
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.

So this issue may be closed with the resume that the example is not supposed to work because of limitations of inverse_of. But a warning or an exception that says "do not do this" would be nice. If you close it you can stop reading here, the code below is trying to get it work using inverse_of on polymorphic association and since it it undefined it can (and does) work strangly.

I play with it more and I found a solution which works for polymorphic association at first glance, but there are a few gotchas. It looks like this (I know it's not ideal but it's the only one which at least create associated record):

class Zoo < ActiveRecord::Base
  has_many :animals, :inverse_of => :zoo
  has_many :lions, :through => :animals, :source => :sort, :source_type => "Lion"

  accepts_nested_attributes_for :lions
end

class Animal < ActiveRecord::Base
  belongs_to :zoo
  belongs_to :sort, :polymorphic => true, :inverse_of => :animal, :class_name => "Lion"

  validates_presence_of :zoo
end

class Lion ## the same as before

The only associations for which inverse_of option must be specified are Zoo->animals and the polymorphic one Animal->sort. The others seems that they don't modify the behaviour. There problems now are following:

Problem 1

Even if the record is created, the attributes are not correctly assigned (:mammal should be true):

zoo = Zoo.create(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])
puts zoo.animals.inspect             # [#<Animal id: 1, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">]
puts zoo.animals.map(&:sort).inspect # [#<Lion id: 1, name: "Simba">]

Problem 2

The class is specified for polymorphic association and if we have also i.e. Tiger, we can't use it. I have tried to solve it by creating a new polymorphic association specific for a lion:

class Animal < ActiveRecord::Base
  belongs_to :zoo
  belongs_to :sort, :polymorphic => true, :inverse_of => :animal, :class_name => "Lion"
  belongs_to :sort_lion, :polymorphic => true, :foreign_key => :sort_id, :foreign_type => :sort_type, :inverse_of => :animal, :class_name => "Lion"

  validates_presence_of :zoo
end

class Zoo < ActiveRecord::Base
  has_many :animals, :inverse_of => :zoo
  has_many :lions, :through => :animals, :source => :sort_lion, :source_type => "Lion"

  accepts_nested_attributes_for :lions
end

class Lion ## the same as before

Now it fails in another way, it creates two records instead of one:

zoo = Zoo.create(:lions_attributes => [{:name => "Simba", :animal_attributes => {:mammal => true}}])

zoo.animals.inspect
# [#<Animal id: 1, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">, #<Animal id: 2, zoo_id: 1, mammal: nil, sort_id: "1", sort_type: "Lion">]

zoo.animals.map(&:sort).inspect
# [#<Lion id: 1, name: "Simba">, #<Lion id: 1, name: "Simba">]

This could be connected with issue #1358

@tom-kuca tom-kuca closed this Jun 1, 2011

jake3030 pushed a commit to jake3030/rails that referenced this issue Jun 28, 2011

Prevent assert_template failures when a render :inline is called befo…
…re rendering a file-based template [#1383 state:resolved]

Signed-off-by: Joshua Peek <josh@joshpeek.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment