我有一套非常简单的HABTM模型
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
def tags= (tag_list)
self.tags.clear
tag_list.strip.split(' ').each do
self.tags.build(:name => tag)
end
end
end
现在一切正常,只是我在Tags表中得到了大量的重复项。
我需要做些什么来避免标签表中的重复(基于名称)?
答案 0 :(得分:40)
以下不会阻止向数据库写入重复关系,它只会确保find
方法忽略重复项。
在Rails 5中:
has_and_belongs_to_many :tags, -> { distinct }
注意:Relation#uniq
在Rails 5中被折旧(commit)
在Rails 4中
has_and_belongs_to_many :tags, -> { uniq }
选项1:防止来自控制器的重复:
post.tags << tag unless post.tags.include?(tag)
但是,多个用户可以同时尝试post.tags.include?(tag)
,因此这受到竞争条件的影响。这是讨论here。
为了获得稳健性,您还可以将其添加到Post模型(post.rb)
def tag=(tag)
tags << tag unless tags.include?(tag)
end
选项2:创建唯一索引
防止重复的最简单方法是在数据库层具有重复约束。这可以通过在表本身上添加unique index
来实现。
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
获得唯一索引后,尝试添加重复记录会引发ActiveRecord::RecordNotUnique
错误。处理此问题超出了本问题的范围。查看this SO question。
rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
答案 1 :(得分:25)
另外建议如上:
:uniq
添加到has_and_belongs_to_many
关联我会做一个明确的检查,以确定该关系是否已经存在。例如:
post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
答案 2 :(得分:21)
在Rails4中:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, -> { uniq }
(注意,-> { uniq }
必须直接在关系名称之后,在其他参数之前)
答案 3 :(得分:20)
您可以将:uniq
选项作为described in the documentation传递。另请注意,:uniq
选项不会阻止创建重复关系,它只会确保访问者/查找方法会选择它们一次。
如果要防止关联表中出现重复项,则应创建唯一索引并处理异常。此外,validates_uniqueness_of无法按预期工作,因为您可以考虑第二个请求在第一个请求检查重复项并写入数据库之间写入数据库的情况。
答案 4 :(得分:13)
设置uniq选项:
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
答案 5 :(得分:5)
我更愿意调整模型并以这种方式创建类:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
然后我将创建包装在逻辑中,以便Tag模型在已经存在的情况下被重用。我甚至可能在标签名称上加上一个唯一的约束来强制执行它。这样可以更有效地搜索任何一种方式,因为您可以只使用连接表上的索引(查找特定标记的所有帖子以及特定帖子的所有标记)。
唯一的问题是您无法重命名标记,因为更改标记名称会影响该标记的所有用途。让用户删除标记并改为创建一个新标记。
答案 6 :(得分:4)
我通过创建一个修复内容的before_save过滤器来解决这个问题。
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
before_save :fix_tags
def tag_list= (tag_list)
self.tags.clear
tag_list.strip.split(' ').each do
self.tags.build(:name => tag)
end
end
def fix_tags
if self.tags.loaded?
new_tags = []
self.tags.each do |tag|
if existing = Tag.find_by_name(tag.name)
new_tags << existing
else
new_tags << tag
end
end
self.tags = new_tags
end
end
end
可以略微优化以使用标签批量工作,也可能需要稍微更好的事务支持。
答案 7 :(得分:2)
这真的很古老,但我想我会分享这样做的方式。
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
在我需要为帖子添加标签的代码中,我执行以下操作:
new_tag = Tag.find_by(name: 'cool')
post.tag_ids = (post.tag_ids + [new_tag.id]).uniq
这样可以根据需要自动添加/删除标签,如果是这种情况,则无需执行任何操作。
答案 8 :(得分:2)
给我工作
覆盖&lt;&lt;关系中的方法
has_and_belongs_to_many :groups do
def << (group)
group -= self if group.respond_to?(:to_a)
super group unless include?(group)
end
end
答案 9 :(得分:1)
提取标记名称以确保安全性。检查标签表中是否存在标签,如果标签不存在,则创建标签:
name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create
然后检查它是否存在于此特定集合中,如果不存在则推送它:
@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
答案 10 :(得分:0)
您应该在tag:name属性上添加索引,然后在Tags#create method中使用find_or_create方法
答案 11 :(得分:0)
在添加记录之前,只需在控制器中添加一个检查。如果是,则不执行任何操作,如果没有,请添加新的:
u = current_user
a = @article
if u.articles.exists?(a)
else
u.articles << a
end
更多:“4.4.1.14 collection.exists?(...)” http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-and-belongs-to-many