I have a pretty simple HABTM set of models
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
The following does not prevent writing duplicate relationships to the database, it only ensures find
methods ignore duplicates.
In Rails 5:
has_and_belongs_to_many :tags, -> { distinct }
Note: Relation#uniq
was depreciated in Rails 5 (commit)
In Rails 4
has_and_belongs_to_many :tags, -> { uniq }
Option 1: Prevent duplicates from the controller:
post.tags << tag unless post.tags.include?(tag)
However, multiple users could attempt post.tags.include?(tag)
at the same time, thus this is subject to race conditions. This is discussed here.
For robustness you can also add this to the Post model (post.rb)
def tag=(tag)
tags << tag unless tags.include?(tag)
end
Option 2: Create a unique index
The most foolproof way of preventing duplicates is to have duplicate constraints at the database layer. This can be achieved by adding a unique index
on the table itself.
rails g migration add_index_to_posts
# migration file
add_index :posts_tags, [:post_id, :tag_id], :unique => true
add_index :posts_tags, :tag_id
Once you have the unique index, attempting to add a duplicate record will raise an ActiveRecord::RecordNotUnique
error. Handling this is out of the scope of this question. View this SO question.
rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
In addition the suggestions above:
:uniq
to the has_and_belongs_to_many
association I would do an explicit check to determine if the relationship already exists. For instance:
post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
You can pass the :uniq
option as described in the documentation. Also note that the :uniq
options doesn't prevent the creation of duplicate relationships, it only ensures accessor/find methods will select them once.
If you want to prevent duplicates in the association table you should create an unique index and handle the exception. Also validates_uniqueness_of doesn't work as expected because you can fall into the case a second request is writing to the database between the time the first request checks for duplicates and writes into the database.
Set the uniq option:
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts , :uniq => true
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags , :uniq => true
To me work
override << method in the relation
has_and_belongs_to_many :groups do
def << (group)
group -= self if group.respond_to?(:to_a)
super group unless include?(group)
end
end
Extract the tag name for security. Check whether or not the tag exists in your tags table, then create it if it doesn't:
name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create
Then check whether it exists within this specific collection, and push it if it doesn't:
@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)