ruby on rails-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 

现在一切正常,除了在Tags表中有大量重复项。

我该怎么做才能避免标签表中的重复(基于名称)?

Sam Saffron asked 2019-11-06T11:56:38Z
12个解决方案
43 votes

仅在视图中防止重复(惰性解决方案)

以下内容不会阻止将重复关系写入数据库,而只能确保ActiveRecord::RecordNotUnique方法忽略重复项。

在Rails 5中:

has_and_belongs_to_many :tags, -> { distinct }

注意:在Rails 5中已弃用ActiveRecord::RecordNotUnique(提交)

在Rails 4中

has_and_belongs_to_many :tags, -> { uniq }

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

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

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

但是,多个用户可以同时尝试ActiveRecord::RecordNotUnique,因此,这取决于比赛条件。 这里讨论。

为了提高鲁棒性,您还可以将其添加到Post模型(post.rb)

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

选项2:创建唯一索引

防止重复的最简单方法是在数据库层具有重复约束。 这可以通过在表本身上添加ActiveRecord::RecordNotUnique来实现。

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错误。 处理这个问题超出了这个范围。 查看此问题。

rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
Jeremy Lynch answered 2019-11-06T11:58:07Z
25 votes

除上述建议外:

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

我将进行显式检查以确定该关系是否已经存在。 例如:

post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
spyle answered 2019-11-06T11:58:56Z
21 votes

在Rails4中:

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

(请注意,-> { uniq }必须直接在关系名称之后,在其他参数之前)

Rails文档

cyrilchampier answered 2019-11-06T11:59:34Z
20 votes

您可以按照文档中的说明传递:uniq选项。 还要注意,:uniq选项不会阻止重复关系的创建,它仅确保访问者/查找方法将选择它们一次。

如果要防止在关联表中重复,则应创建唯一索引并处理异常。 另外validates_uniqueness_of不能按预期工作,因为您可能会遇到在第一个请求检查重复并写入数据库之间,第二个请求正在写入数据库的情况。

Simone Carletti answered 2019-11-06T12:00:06Z
13 votes

设置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
Joshua Cheek answered 2019-11-06T12:00:32Z
5 votes

我希望通过这种方式调整模型并创建类:

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模型(如果已经存在)可以重用。 我什至可能会对标签名称设置唯一的约束以强制实施。 由于您仅可以使用联接表上的索引(查找特定标签的所有帖子,以及特定帖子的所有标签),因此这两种方式均可更有效地进行搜索。

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

Jeff Whitmire answered 2019-11-06T12:01:12Z
4 votes

我通过创建一个可以解决问题的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

可以对其进行稍微优化以使其与标签一起批量使用,也可能需要一些更好的事务支持。

Sam Saffron answered 2019-11-06T12:01:45Z
2 votes

这确实很老,但我想我会分享自己的方式。

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

这样的效果是可以根据需要自动添加/删除标签,或者在这种情况下不执行任何操作。

Javeed answered 2019-11-06T12:02:25Z
2 votes

对我来说

  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
    
Jose Fuentes Delgado answered 2019-11-06T12:03:08Z
1 votes

提取标签名称以确保安全。 检查标签是否存在于您的标签表中,如果不存在则创建它:

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

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

@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
dav1dhunt answered 2019-11-06T12:03:43Z
0 votes

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

docs

ajbraus answered 2019-11-06T12:04:15Z
0 votes

只需在添加记录之前在您的控制器中添加一个检查即可。 如果可以,则不执行任何操作,如果不执行,请添加新的:

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]

Matthew Bennett answered 2019-11-06T12:04:48Z
translate from https://stackoverflow.com:/questions/1129781/has-and-belongs-to-many-avoiding-dupes-in-the-join-table