Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

'validates_uniqueness_of' ignored if 'accepts_nested_attributes_for' is used #1572

Closed
adamflorin opened this Issue · 25 comments
@adamflorin

When batch creating a number of objects (as is done via mass assignment with accepts_nested_attributes_for), it looks like validates_uniqueness_of is just checking the db for conflicts, but not looking for conflicts within the batch currently being saved.

For example, given these two models:

class Parent < ActiveRecord::Base
  has_many :children
  accepts_nested_attributes_for :children
end

class Child < ActiveRecord::Base
  belongs_to :parent
  validates_uniqueness_of :name
end

Here's my output from the console. See that, despite the uniqueness constraint, two children with the same name were created, and that they then can't be re-saved!

?> p = Parent.create
=> #<Parent id: 3, created_at: "2011-06-08 17:31:54", updated_at: "2011-06-08 17:31:54">
>> p.update_attributes :children_attributes => [{:name => "Kid"}, {:name => "Kid"}]
=> true
>> Child.all.map(&:name)
=> ["Kid", "Kid"]
>> c = Child.first
=> #<Child id: 4, parent_id: 3, name: "Kid", created_at: "2011-06-08 17:32:12", updated_at: "2011-06-08 17:32:12">
>> c.save
=> false
>> c.errors.full_messages
=> ["Name has already been taken"]

I understand it may not be the role of validates_uniqueness_of to validate these objects against each other, but is there at least a reasonable workaround short of hand-coding a list-sorter/de-duper or something? Thanks!

@raviolicode

I can reproduce the problem, but I don't think it's directly related to nested_atributes_for. It also happens associating the children manually:

?> p = Parent.create
=> #<Parent id: 5, name: nil, age: nil, created_at: "2011-06-16 19:09:53", updated_at: "2011-06-16 19:09:53">
>> p.children.build :name => "Kid"
=> #<Child id: nil, parent_id: 5, name: "Kid", age: nil, created_at: nil, updated_at: nil>
>> p.children.build :name => "Kid"
=> #<Child id: nil, parent_id: 5, name: "Kid", age: nil, created_at: nil, updated_at: nil>
>> p.children
=> [#<Child id: nil, parent_id: 5, name: "Kid", age: nil, created_at: nil, updated_at: nil>, #<Child id: nil, parent_id: 5, name: "Kid", age: nil, created_at: nil, updated_at: nil>]
>> p.save
=> true
>> Child.all.map(&:name)
=> ["Kid", "Kid"]

You could prevent this behavior with validates_associated:

class Parent < ActiveRecord::Base
  has_many :children
  validates_associated :children
  accepts_nested_attributes_for :children
end

Hope that's what you're looking for.

@sikachu
Collaborator

Yep, validates_associated is the key here.

@sikachu sikachu closed this
@verto

I'm sorry guys, but i have same problem. I got reproduced the problem of @adamflorin with the validates_associated defined. The problem only occurs with nested_attributes. So, can't i use nested_attribute? :S

ruby-1.9.2-p290 :001 > p = Parent.create :children_attributes => [{:name => "Kid"}, {:name => "Kid"}]
=> #<Parent id: 1, created_at: "2011-09-18 18:00:08", updated_at: "2011-09-18 18:00:08"> 
ruby-1.9.2-p290 :002 > Child.all.map(&:name)
 => ["Kid", "Kid"] 
ruby-1.9.2-p290 :003 > c = Child.first
 => #<Child id: 1, parent_id: 1, name: "Kid", created_at: "2011-09-18 18:00:08", updated_at: "2011-09-18 18:00:08"> 
ruby-1.9.2-p290 :004 > c.save
 => false

Models:

class Parent < ActiveRecord::Base
  has_many :children
  validates_associated :children
  accepts_nested_attributes_for :children
end

class Child < ActiveRecord::Base
  belongs_to :parent
  validates_uniqueness_of :name
end

i'm using rails 3.1.0.

Thanks!

@celsodantas

Same problem over here.

Even with validates_associated Rails let the Child be saved.

p = Parent.new
p.children << Child.new(:name => "Kid")
p.children << Child.new(:name => "Kid")
p.save
=> true

Rails (ActiveRecord) should avoid it, right? It only check if the Children named "Kid" is already in DB but not in memory.

There's a workaround using this: http://stackoverflow.com/questions/2772236/validates-uniqueness-of-in-nested-model-rails#answers-header

But I think it's a ActiveRecord issue.

@rafamvc

+1

@kevinrood

+1 to fix please

@emptyflask

+1, running into the same problem here, and validates_associated doesn't solve it

@billywatson

+1 can we reopen this issue, please?

@adsummos

validates_associated works fine with validates_uniqueness of if you are adding a single record to a collection. Where it breaks is when you use collection.build to add multiple records. If there are duplicates within the new records, the validation doesn't find them.

@fj
fj commented

+1 for reopening; validates_associated doesn't work. To summarize, the issue is that if you:

  • are submitting multiple new records as part of an association; and
  • you accepts_nested_attribues_for on that association; and
  • that association has a validates ..., :uniqueness on it

then the uniqueness validator:

  • checks against records already in the database
  • won't find records that are not unique within the collection being submitted, since they aren't in the database yet

Likewise, validates_associated is of no help since it doesn't address the underlying uniqueness issue.

@rares

+1 for a re-open.

At the very least, the validates_uniqueness_of documentation should point out that its behavior is altered significantly when used conjunction with accepts_nested_attributes_for and or when :autosave is enabled.

If I get a chance in the next few days, i'll fork and add the warning myself.

@billywatson

@rares, I think the warning might be unnecessary as this seems to be a bug, not just a slight altering in behavior. The validation just plain doesn't work with acceps_nested_attributes_for.

@rares

I am split on this, as where it seems like a bug, it is actually really hard to solve in the context of accepts_nested_attributes_for because it all executes within the scope of one transaction. The query executed in the validation never can see the data because the data is not committed (default transaction isolation level is READ_COMMITTED on oracle and postgres, not sure about mysql).

One thought I have had is to alter the transaction isolation level (to READ_UNCOMMITTED), but can lead to some scary situations in a write-heavy app.

validates_uniqueness_of can also be extended to look at its in-memory collection for uniqueness as well, but that is more of a hack then actually fixing the problem.

Bottom line, my reasoning for including it in the docs was not to be the eventual solution (as i don't think its easily solvable given how both nested_attributes and the validator works) but as warning so that people are at least educated when using these two features together.

@adsummos

It's important to note that this is not specific to accepts_nested_attributes_for. This is a general problem with validates_uniqueness_of when you are adding multiple records to a collection. The transaction doesn't matter because it's the in-memory objects that are not being validated against each other.

@rares

Yeah, that is much is understood. the question is how to merge that concern with that validates_uniqueness_of does.

@adsummos

It's really not a difficult problem to solve, we already have a patch in our own code. A generic solution for AR is not much more complicated than what we are doing. If nothing else gets done, when I have time I will submit something myself.

@rares

Great! I look forward to it.

@pekeler

+1

@popungco

+1 validates_presence_of too.

@morexchange

For the record, this happens not only for validates_uniqueness_of but for other validations as well (all?).

In my case, I have a validates_numericality_of :x, :only_integer => true on one of the attributes (data type: integer) of the associated object. When value is changed from 200 (valid value) to 200.13 (decimal is invalid), it doesn't trigger "not a number" error.

Surprisingly, when value is changed from 200 to 199.13 (still invalid but the integer portion is different from previous value), the validations run since the field is of type :integer and activerecord is able to detect that integer portion of value has changed.

Rails version: 2.3.14

@Backoo

+1 ! ! !

@birula

+1

@jeyb

I opened a pull request with a fix for this bug, take a look. Let me know if you have suggestions or comments.

@sotrachhun

I have used this patch in my code and it seem works well on Development and Staging Environment.
However, on Production I faced some problems but not sure it is related to this patch or not.
User add nested records after saved it responded ok but it doesn't save any data. After users try 2 or 3 times, it saved. It is happen only some time and not often.

class Author
  has_many :books

  # Could easily be made a validation-style class method of course
  validate :validate_unique_books

  def validate_unique_books
    validate_uniqueness_of_in_memory(
      books, [:title, :isbn], 'Duplicate book.')
  end
end

module ActiveRecord
  class Base
    # Validate that the the objects in +collection+ are unique
    # when compared against all their non-blank +attrs+. If not
    # add +message+ to the base errors.
    def validate_uniqueness_of_in_memory(collection, attrs, message)
      hashes = collection.inject({}) do |hash, record|
        key = attrs.map {|a| record.send(a).to_s }.join
        if key.blank? || record.marked_for_destruction?
          key = record.object_id
        end
        hash[key] = record unless hash[key]
        hash
      end
      if collection.length > hashes.length
        self.errors.add_to_base(message)
      end
    end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.