has_and_belongs_to_many,避免连接表中的欺骗

时间:2009-07-15 06:47:58

标签: ruby-on-rails activerecord

我有一套非常简单的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表中得到了大量的重复项。

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

12 个答案:

答案 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)

另外建议如上:

  1. :uniq添加到has_and_belongs_to_many关联
  2. 在联接表上添加唯一索引
  3. 我会做一个明确的检查,以确定该关系是否已经存在。例如:

    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 }必须直接在关系名称之后,在其他参数之前)

Rails documentation

答案 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)

给我工作

  1. 在联接表上添加唯一索引
  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方法

docs

答案 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