HABTM - uniqueness constraint
Asked Answered
S

4

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?

Shawnna answered 14/2, 2011 at 3:38 Comment(0)
H
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.

Halsy answered 14/2, 2011 at 13:22 Comment(5)
Thanks for this! I ended up using this for a HABTM instance of a user and groups (essentially the same kind of role setup).Aridatha
It sucks a little that deduplication isn't automatically handled for habtm associations, unlike :has_many.Periosteum
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.Halsy
@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. :)Tomasine
@ArtShayderov I'm facing the same issue mentioned by Inquisitive.Suicidal
M
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
Mouthful answered 22/9, 2016 at 0:29 Comment(6)
Works, but doesn't look very pretty, does it?Rivarivage
+1 for auto rescuing Unique errors. there's no need for a duplicate HABTM to prevent saving other attributesIsomerous
This is the only solution that actually is working in Rails 6Gabriellia
The only solution that works on rails 7Imes
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.Alcaide
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.Alcaide
I
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)

Irredeemable answered 14/2, 2011 at 3:57 Comment(0)
S
5

I prefer

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

other options reference here

Surprisal answered 26/5, 2014 at 5:11 Comment(4)
Trying this way gives me the error wrong number of arguments 1 for oTomasine
it's available for rails 4, not rails 3Overcrop
But this validaton is in model level only . DB level validation is needed to handle concurrent requestsDissemble
Using -> {uniq} helps displaying only one of the repeated records but duplication still occurs.Reunion

© 2022 - 2024 — McMap. All rights reserved.