Snackmoore
Snackmoore

Reputation: 935

Setup and Testing to prevent duplication in ActiveRecord many to many relationship

I have the following classes for a many to many relationship between "Item" and "Color".

And "Item" should not have duplicated "Colors", for example:- If "Item1" has "Blue" & "Red" then we cannot add another "Red" to "Item1"

Is this the correct way to set this up?

class Item < ActiveRecord::Base  
    has_many :item_colors  
    has_many :colors, :through => item_colors  
end  

class Color < ActiveRecord::Base
    has_many :item_colors
    has_many :items, :through => item_colors
end

class ItemColor < ActiveRecord::Base
    belongs_to :item
    belongs_to :color

    validates_uniqueness_of :color, scope: :item
end

My Test for duplicated colors. Is it how to test it?

describe "item should not have duplicated colors" do
    before do
        @item = FactoryGirl.create(:item)
        @color1 = FactoryGirl.create(:color)
        @item.colors << @color1
        @item.colors << @color1
        @item.save
    end

    it { should_not be_valid }
end

When I try this in rails console, it will fail when I add duplcated color to an item but instead of getting an error message in item.errors.message, I got an ActiveRecord exception

"ActiveRecord::RecordInvalid: Validation failed: Color has already been taken"

Please advise.

Upvotes: 3

Views: 95

Answers (1)

wicz
wicz

Reputation: 2313

When you add the second color, it is automatically saved because the parent object @item is already saved, i.e. it is not a new_record.

Given it is a has_many :through association, it is always saved with the bang version of save!, which in turn raises the exception because your join model ItemColor fails on the validation of uniqueness.

In your case you have two options:

  1. rescue the exception and manage the error messages manually or;
  2. if you're using a join model just to add the validation layer, you could get rid of it, use a HABTM instead and handle the association as a set, e.g.

    > item = FactoryGirl.create(:item)
    > color = FactoryGirl.create(:color)
    > 10.times { item.colors |= [color] } # you can add it n times...
    > item.colors.count # => 1  ...still only one is saved b/c it is a union set.
    

How does that sound to you?

UPDATE: In case you really want to show an error message, you could, e.g.

if item.colors.include?(color)
  item.errors.add(:colors, "color already selected")
else
  item.colors |= [color]
end

Upvotes: 3

Related Questions