rails: 'validates_uniqueness_of' ignored if 'accepts_nested_attributes_for' is used

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!

About this issue

  • Original URL
  • State: closed
  • Created 13 years ago
  • Reactions: 4
  • Comments: 41 (4 by maintainers)

Most upvoted comments

The issue is still present 8 years later in Rails 6.

https://github.com/toptal/database_validations -> validates_db_uniqueness_of did solve the issue.

Thank you

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

Hi, I’d suggest you use validates_db_uniqueness_of. It should solve your problem. Let me know if it’s not. The validator is not completely ready: some of the options are not supported yet but will be very soon.

Any feedback is appreciated 👍

Can we reopen this?

Hi @djezzzl, thanks a lot for your explanation! I’ve already went to this direction to iterate children and retrieve error message from there.

Just got into this problem too

+1 for reopening, a simple iteration / group_by would solve it

@jamesst20 Thank you, glad it helps, more cool staff coming soon 👍

P.S. In case, you work with FactoryBot I can suggest you to try https://github.com/djezzzl/factory_trace And, if you like to have your DB to be consistent with models definition, there is a https://github.com/djezzzl/database_consistency.

@alexander-e1off

Hi, thank you for your case!

First of all, I want to highlight that the gem solves the real problem: now we don’t save duplicates and save returns false.

About your issue: this is happening because of the implementation of accepts_nested_attributes_for. The flow is the following:

  • It runs valid? on every sub-entity. Each valid? returns true because there is no duplication yet (we didn’t commit the transaction to a database).
  • Because everything is fine it starts an insert transaction (saving).
  • Then the transaction is rolled back because of internal duplicates. And we catch it and returns false.

There is an option (even if a bit ugly) to get the errors:

p = Parent.new(parent_children: [ParentChild.new(child: Child.first), ParentChild.new(child: Child.first)])
p.save
# => false
p.parent_children.map(&:errors)
 # => [#<ActiveModel::Errors:0x00007fa7d2479c40 @base=#<ParentChild id: nil, parent_id: 1, child_id: 1, any_data: nil, created_at: "2018-09-14 20:05:43", updated_at: "2018-09-14 20:05:43">, @messages={}, @details={}>, #<ActiveModel::Errors:0x00007fa7d3466130 @base=#<ParentChild id: nil, parent_id: 1, child_id: 1, any_data: nil, created_at: "2018-09-14 20:05:43", updated_at: "2018-09-14 20:05:43">, @messages={:child_id=>["has already been taken"]}, @details={:child_id=>[{:error=>:taken, :value=>1}]}>] 

As you can see, first sub-entity doesn’t have any errors but second does, you can extract it and use.

I hope this explanation helps you.

This just bit me as well. Thankfully, I had an index in place to prevent the “bad” data from being saved. However, it’d make for a much better user experience if the issue could be caught during the ActiveRecord validation cycle.

+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.

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.

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.