17

I have two models with a HABTM relationship - User and Role.

  • user - has_and_belongs_to_many :roles
  • role - belongs_to :user

I want to add a uniqueness constraint in the join (users_roles table) that says the user_id and role_id must be unique. In Rails, would look like:

validates_uniqueness_of :user, :scope => [:role]

Of course, in Rails, we don't usually have a model to represent the join relationship in a HABTM association.

So my question is where is the best place to add the constraint?

keruilin
  • 16,782
  • 34
  • 108
  • 175

4 Answers4

38

You can add uniqueness to join table

add_index :users_roles, [ :user_id, :role_id ], :unique => true, :name => 'by_user_and_role'

see In a join table, what's the best workaround for Rails' absence of a composite key?

Your database will raise an exception then, which you have to handle.
I don't know any ready to use rails validation for this case, but you can add your own validation like this:

class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :before_add => :validates_role

I would just silently drop the database call and report success.

def validates_role(role)
  raise ActiveRecord::Rollback if self.roles.include? role
end

ActiveRecord::Rollback is internally captured but not reraised.

Edit

Don't use the part where I'm adding custom validation. It kinda works but there is better alternatives.

Use :uniq option on association as @Spyros suggested in another answer:

class Parts < ActiveRecord::Base
  has_and_belongs_to_many :assemblies, :uniq => true, :read_only => true
end  

(this code snippet is from Rails Guides v.3). Read up on Rails Guides v 3.2.13 look for 4.4.2.19 :uniq

Rails Guide v.4 specifically warns against using include? for checking for uniqueness because of possible race conditions.

The part about adding an index to join table stays.

Community
  • 1
  • 1
Art Shayderov
  • 5,002
  • 1
  • 26
  • 33
  • Thanks for this! I ended up using this for a HABTM instance of a user and groups (essentially the same kind of role setup). – dennismonsewicz May 09 '12 at 03:31
  • 1
    It sucks a little that deduplication isn't automatically handled for habtm associations, unlike `:has_many`. – prusswan Jul 04 '13 at 04:09
  • 1
    posted after edit. I posted this answer and forgot about it. And then I came across a related problem and I remembered that I have an answer people keep upvoting and I checked it and OMG! What was I thinking! I hope that nobody has any problems because he used my answer. I feel bad enough without that. Well that's a lesson for all of us. You leave a sloppy answer in a hurry on Stackoverflow and forget about it but time will come it'll spring out at you and bite you in the ass. – Art Shayderov Sep 21 '13 at 09:12
  • 1
    @ArtShayderov My code was running for some time, when I realized that duplicate entries are getting inserted in my join table. Then I used :uniq => true constraint on both the Models, Product & Categories, having habtm with each other, but it does not seem to affect much, I can still enter duplicate records. Do I need to restart something? I restarted the server. I store the data as product.categories << category1, where I fetch category1 by some query. Any help will be appreciated. Thanks. :) – inquisitive Dec 25 '14 at 10:49
  • @ArtShayderov I'm facing the same issue mentioned by Inquisitive. – Alfie May 06 '16 at 21:56
11

In Rails 5 you'll want to use distinct instead of uniq

Also, try this for ensuring uniqueness

has_and_belongs_to_many :foos, -> { distinct } do
  def << (value)
    super value rescue ActiveRecord::RecordNotUnique
  end
end
Caleb
  • 3,692
  • 3
  • 24
  • 28
  • 2
    Works, but doesn't look very pretty, does it? – Fritzz Nov 25 '16 at 09:40
  • 2
    +1 for auto rescuing Unique errors. there's no need for a duplicate HABTM to prevent saving other attributes – TheRealMrCrowley Sep 05 '17 at 21:56
  • 3
    This is the only solution that actually is working in Rails 6 – István Ujj-Mészáros Mar 26 '20 at 01:23
  • 2
    The only solution that works on rails 7 – Almokhtar Mar 20 '22 at 14:48
  • This seems to be an incorrect use of an inline rescue. It appears you're trying to rescue from a particular type of error, but inline rescues always rescue from all standard errors and the expression to the right of `rescue` is what then gets evaluated when a error is resuced. – Toby 1 Kenobi Nov 25 '22 at 11:37
  • Another, more significant failing in this answer is that, even though the `foo` didn't get added to the collection in the database, it remains connected at the application level, so next time any modifaction on the object is done ActiveRecord will try to add it again and the DB error will be raised. `bar.foos << already_connected_foo; bar.save` will raise the error. – Toby 1 Kenobi Nov 28 '22 at 03:03
5

I prefer

class User < ActiveRecord::Base
  has_and_belongs_to_many :roles, -> { uniq }
end

other options reference here

ken
  • 787
  • 8
  • 14
5

I think that using :uniq => true would ensure that you get no duplicate objects. But, if you want to check on whether a duplicate exists before writing a second one to your db, i would probably use find_or_create_by_name_and_description(...).

(Of course name and description are your column values)

Spyros
  • 46,820
  • 25
  • 86
  • 129