10

A have two models, "shop" and "product", linked via has_many :through.

In the shop form there are nested attributes for multiple products, and I'm having a little trouble with the product's uniqueness validation. If I enter a product, save it, then try to enter the same name for a new product, the uniqueness validation triggers successfully.

However, if I enter the same product name in 2 rows of the same nested form, the form is accepted - the uniqueness validation doesn't trigger.

I'm guessing this is a fairly common problem, but I can't find any simple solution. Anyone have any suggestions on the easiest way to ensure uniqueness validations are obeyed within the same nested form?

Edit: Product model included below

class Product < ActiveRecord::Base

  has_many :shop_products
  has_many :shops, :through => :shop_products

  validates_presence_of :name
  validates_uniqueness_of :name
end
PlankTon
  • 12,443
  • 16
  • 84
  • 153
  • You always can ([and should!](http://railswarts.blogspot.com/2007/11/validatesuniquenessof-is-broken-and.html)) back uniqueness validation up with a unique index in your DB. It would stop duplicates like you're seeing, but it wouldn't do it nicely - it would just throw an exception on save... Maybe you could write a custom validation function to take care of this? – Xavier Holt Mar 30 '11 at 07:03
  • What does your `product` look like? – Jeffrey W. Mar 30 '11 at 07:03
  • Jeffrey: Product model added above – PlankTon Mar 30 '11 at 07:10
  • Xavier: *nods* Cheers. I'll definitely throw an index into the DB. Looks like a custom validation may be the solution...just a little surprised there doesn't seem to be anything built in. – PlankTon Mar 30 '11 at 07:11

3 Answers3

17

To expand on Alberto's solution, the following custom validator accepts a field (attribute) to validate, and adds errors to the nested resources.

# config/initializers/nested_attributes_uniqueness_validator.rb
class NestedAttributesUniquenessValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value.map(&options[:field]).uniq.size == value.size
      record.errors[attribute] << "must be unique"
      duplicates = value - Hash[value.map{|obj| [obj[options[:field]], obj]}].values
      duplicates.each { |obj| obj.errors[options[:field]] << "has already been taken" }
    end
  end
end

# app/models/shop.rb
class Shop < ActiveRecord::Base
  validates :products, :nested_attributes_uniqueness => {:field => :name}
end
Rob DiCiuccio
  • 326
  • 2
  • 7
  • Hi, I got the similar problem and tried your code and i got `Unknown validator: 'NestedAttributesUniquenessValidator'`. Any idea? – Sri Jul 17 '13 at 11:25
  • 1
    Thanks for a great solution! It only needed a minor tweak re _destroy key support for `accepts_nested_attributes_for :items, allow_destroy: true` case - that's line 3 at [my gist](https://gist.github.com/artemv/4993b128c1a25f06d5d0) – Artem Vasiliev Mar 17 '16 at 19:06
  • 2
    Awesome - very smart solution. In Rails 4 I needed to add ```to_a``` right before ```uniq```, probably because of a deprecation. I tweaked my version of this a bit to add the validation error on every nested object that is both a duplicate and is new (i.e. preexisting objects are not to blame - just the newly added duplicates). My gist is here: https://gist.github.com/francirp/01d2b82c3000cce626f0f34bdcf5c33c – Ryan Francis Apr 10 '16 at 17:01
14

You could write a custom validator like

# app/validators/products_name_uniqueness_validator.rb
class ProductsNameUniquenessValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << "Products names must be unique" unless value.map(&:name).uniq.size == value.size
  end
end

# app/models/shop.rb
class Shop < ActiveRecord::Base
  validates :products, :products_name_uniqueness => true
end
Alberto Santini
  • 6,425
  • 1
  • 26
  • 37
0

I found the answer over here :

https://rails.lighthouseapp.com/projects/8994/tickets/2160-nested_attributes-validates_uniqueness_of-fails

Syed
  • 15,657
  • 13
  • 120
  • 154