1

Given a Dinner model that has many Vegetable models, I would prefer that

dinner.vegetables << carrot

not add the carrot if

dinner.vegetables.exists? carrot

Yet it does. It will add a duplicate record every time << is called.

There is a :uniq option you can set on the association, but it only FETCHES AND RETURNS one result if there are multiples, it doesn't ENFORCE unique values.

I could check for exists? every time I add an obj to a collection, but that is tedious and error-prone.

How can I use << freely and not worry about errors and not check for already existing collection members every time?

pixelearth
  • 13,674
  • 10
  • 62
  • 110
  • possible duplicate of [Rails idiom to avoid duplicates in has_many :through](http://stackoverflow.com/questions/1315109/rails-idiom-to-avoid-duplicates-in-has-many-through) – Peter Brown Jul 24 '12 at 03:42

4 Answers4

2

The best way is to use Set instead of Array:

set = Set.new
set << "a"
set << "a"
set.count  -> returns 1
bender
  • 1,430
  • 10
  • 14
  • 1
    Yeah, this approach is really usefull - don't mention simple things, because only very complex solutions are good, right? – bender Aug 30 '11 at 19:02
  • Petr, I can't tell if you're being sarcastic here? But I think if I understand you, you would then do dinner.vegetables << set? – pixelearth Sep 01 '11 at 16:05
  • bender, I think I didn't clarify that the collection is an active record association... not just an array. – pixelearth Jul 30 '12 at 02:43
1

You can add an ActiveRecord unique constraint if you have a join model representing a many-to-many relationship between dinners and vegetables. That's one reason I use join models and has_many :through as opposed to has_and_belongs_to_many. It's important to add a uniqueness constraint at the database level if possible.

UPDATE:

To use a join model to enforce constraint you would need an additional table in your database.

class Dinner
  has_many :dinner_vegetables
  has_many :vegetables, :through => :dinner_vegetables
end

class Vegetable
  has_many :dinner_vegetables
  has_many :dinners, :through => :dinner_vegetables
end

class DinnerVegetable
  belongs_to :dinner
  belongs_to :vegetable

  validates :dinner_id, :uniqueness => {:scope => :vegetable_id} # You should also set up a matching DB constraint
end
Wizard of Ogz
  • 12,543
  • 2
  • 41
  • 43
  • @pixelearth Sorry, my answer didn't go into much detail. I added some code to it which shows how the join model and uniqueness constraint would work. – Wizard of Ogz Sep 02 '11 at 14:27
0

The other posters' ideas are fine, but as another option you can also enforce this on the database level using e.g. the UNIQUE constraint in MySQL.

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
0

After a lot of digging, I've discovered something cool: before_add, which is an association callback, which I never knew even existed. So I could do something like this:

  has_many :vegetables, :before_add => :enforce_unique

  def enforce_unique(assoc)
    if exists? assoc
    ...
  end

Doing this at the DB level is a great idea if you REALLY NEED this to be unique, but in the case that it's not mission critical the solution above is enough for me.

It's mostly to avoid the icky feeling of having extra records lying around in the db...

pixelearth
  • 13,674
  • 10
  • 62
  • 110
  • Ah, very good. This is very nice for things like enforcing a constraint on the number of associated records or checking distinct records for matching fields. I don't see how this will avoid extra records in your case. – Wizard of Ogz Sep 02 '11 at 14:37
  • Your `Dinner` to `Vegetable` relationship is many-to-many, correct? I realized that you had never actually stated this, but it's the only case that really makes sense for the problem. – Wizard of Ogz Sep 02 '11 at 14:38