has_and_belongs_to_many,避免连接表中的愚蠢

我有一个非常简单的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 

现在一切正常,除了我在标签表中得到了大量的重复。

我需要做些什么来避免标签表中的重复(基于名称)?

另外上面的build议:

  1. :uniq添加到has_and_belongs_to_many关联中
  2. 在连接表上添加唯一索引

我会做一个明确的检查,以确定关系是否已经存在。 例如:

 post = Post.find(1) tag = Tag.find(2) post.tags << tag unless post.tags.include?(tag) 

在Rails4中:

 class Post < ActiveRecord::Base has_and_belongs_to_many :tags, -> { uniq } 

(注意, -> { uniq }必须在关系名之后,在其他params之前)

Rails文档

您可以按照文档中所述传递:uniq选项。 另请注意:uniq选项不会阻止重复关系的创build,它只会确保访问器/查找方法将select一次。

如果你想防止关联表中的重复,你应该创build一个唯一的索引并处理exception。 另外validates_uniqueness_of不能按预期工作,因为您可能陷入第一个请求检查重复项并写入数据库之间的第二个请求正在写入数据库的情况。

仅在视图中防止重复(懒惰解决scheme)

以下不会阻止向数据库写入重复关系,它只会确保find方法忽略重复。

在Rails 5中:

 has_and_belongs_to_many :tags, -> { distinct } 

注意: Relation#uniq在Rails 5中被折旧( 提交 )

在Rails 4中

 has_and_belongs_to_many :tags, -> { uniq } 

防止保存重复数据 (最佳解决scheme)

选项1:防止控制器重复:

 post.tags << tag unless post.tags.include?(tag) 

但是,多个用户可以同时尝试post.tags.include?(tag) ,因此这受到竞争条件的限制。 这在这里讨论。

为了健壮性,您还可以将其添加到Post模型(post.rb)

 def tag=(tag) tags << tag unless tags.include?(tag) end 

选项2:创build一个唯一的索引

防止重复的最简单的方法是在数据库层有重复的约束。 这可以通过在表上添加一个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 

一旦你有唯一的索引,试图添加一个重复的logging将引发一个ActiveRecord::RecordNotUnique错误。 处理这个问题超出了这个问题的范围。 查看这个SO问题 。

 rescue_from ActiveRecord::RecordNotUnique, :with => :some_method 

设置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 

我宁愿调整模型,并以这种方式创build类:

 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 

然后,我会将创build包装在逻辑中,以便Tag模型在已经存在的情况下被重用。 我甚至可能会在标签名称上加上一个唯一的约束来强制执行它。 这样可以更有效地search任何一种方式,因为您可以使用连接表上的索引(查找特定标签的所有post以及特定post的所有标签)。

唯一的问题是,您不能允许重命名标签,因为更改标签名称会影响该标签的所有用途。 让用户删除标签,并创build一个新的标签。

我通过创build一个before_savefilter来解决这个问题。

 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 

它可以稍微优化,与标签批量工作,也可能需要稍微更好的事务支持。

给我工作

  1. 在连接表上添加唯一索引
  2. 覆盖关系中的<<方法

     has_and_belongs_to_many :groups do def << (group) group -= self if group.respond_to?(:to_a) super group unless include?(group) end end 

提取标签名称的安全性。 检查标记表中是否存在标记,如果不存在,则创build标记:

 name = params[:tag][:name] @new_tag = Tag.where(name: name).first_or_create 

然后检查它是否存在于此特定集合中,如果不存在,则将其推入:

 @taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag) 

这真的很老,但我认为我会分享我的做法。

 class Tag < ActiveRecord::Base has_and_belongs_to_many :posts end class Post < ActiveRecord::Base has_and_belongs_to_many :tags end 

在我需要给post添加标签的代码中,我做了如下的操作:

 new_tag = Tag.find_by(name: 'cool') post.tag_ids = (post.tag_ids + [new_tag.id]).uniq 

这具有根据需要自动添加/移除标签的效果,或者如果是这样的话,则不做任何事情。

您应该在标签:name属性上添加一个索引,然后在Tags#create方法中使用find_or_create方法

文档