has_and_belongs_to_many, avoiding dupes in the join table
我有一套非常简单的HABTM模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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表中得到了大量的重复项。
我需要做些什么来避免标签表中的重复(基于名称)?
仅在视图中防止重复(Lazy解决方案)
以下内容不会阻止将重复关系写入数据库,它只能确保
在Rails 5中:
1 | has_and_belongs_to_many :tags, -> { distinct } |
注意:
在Rails 4中
1 | has_and_belongs_to_many :tags, -> { uniq } |
防止保存重复数据(最佳解决方案)
选项1:防止来自控制器的重复:
1 | post.tags << tag unless post.tags.include?(tag) |
但是,多个用户可能同时尝试
为了增强稳定性,您还可以将其添加到Post模型(post.rb)
1 2 3 | def tag=(tag) tags << tag unless tags.include?(tag) end |
选项2:创建唯一索引
防止重复的最简单方法是在数据库层具有重复的约束。这可以通过在表本身上添加
1 2 3 4 | 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 |
获得唯一索引后,尝试添加重复记录将引发
1 | rescue_from ActiveRecord::RecordNotUnique, :with => :some_method |
另外上面的建议:
我会做一个明确的检查,以确定该关系是否已经存在。例如:
1 2 3 | post = Post.find(1) tag = Tag.find(2) post.tags << tag unless post.tags.include?(tag) |
在Rails4中:
1 2 | class Post < ActiveRecord::Base has_and_belongs_to_many :tags, -> { uniq } |
(注意,
Rails文档
您可以按照文档中的说明传递
如果要防止关联表中出现重复,则应创建唯一索引并处理异常。此外,validates_uniqueness_of无法按预期工作,因为您可以考虑第二个请求在第一个请求检查重复项并写入数据库之间写入数据库的情况。
设置uniq选项:
1 2 3 4 5 6 | 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 |
我更愿意调整模型并以这种方式创建类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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模型在已经存在的情况下被重用。我甚至可能在标签名称上加上一个唯一的约束来强制执行它。这样可以更有效地搜索任何一种方式,因为您只需使用连接表上的索引(查找特定标记的所有帖子,以及特定帖子的所有标记)。
唯一的问题是您不能允许重命名标记,因为更改标记名称会影响该标记的所有使用。让用户删除标签并改为创建一个新标签。
我通过创建一个修复内容的before_save过滤器来解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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 3 4 5 6 | has_and_belongs_to_many :groups do def << (group) group -= self if group.respond_to?(:to_a) super group unless include?(group) end end |
这真的很古老,但我想我会分享这样做的方式。
1 2 3 4 5 6 7 | class Tag < ActiveRecord::Base has_and_belongs_to_many :posts end class Post < ActiveRecord::Base has_and_belongs_to_many :tags end |
在我需要为帖子添加标签的代码中,我执行以下操作:
1 2 | new_tag = Tag.find_by(name: 'cool') post.tag_ids = (post.tag_ids + [new_tag.id]).uniq |
这具有根据需要自动添加/删除标签的效果,或者如果是这种情况则不执行任何操作。
提取标记名称以确保安全性。检查标记表中是否存在标记,如果不存在,则创建标记:
1 2 | name = params[:tag][:name] @new_tag = Tag.where(name: name).first_or_create |
然后检查它是否存在于此特定集合中,如果不存在则将其推送:
1 | @taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag) |
只需在添加记录之前在控制器中添加一个检查。如果是,则不执行任何操作,如果没有,请添加新的:
1 2 3 4 5 6 7 | 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
您应该在tag:name属性上添加索引,然后在Tags#create方法中使用find_or_create方法
文档