has_and_belongs_to_many,避免连接表中的欺骗

Sam*_*ron 57 activerecord ruby-on-rails

我有一套非常简单的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 
Run Code Online (Sandbox Code Playgroud)

现在一切正常,除了我在Tags表中得到了大量的重复项.

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

Jer*_*nch 43

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

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

在Rails 5中:

has_and_belongs_to_many :tags, -> { distinct }
Run Code Online (Sandbox Code Playgroud)

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

在Rails 4中

has_and_belongs_to_many :tags, -> { uniq }
Run Code Online (Sandbox Code Playgroud)

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

选项1:防止来自控制器的重复:

post.tags << tag unless post.tags.include?(tag)
Run Code Online (Sandbox Code Playgroud)

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

为了增强稳定性,您还可以将其添加到Post模型(post.rb)

def tag=(tag)
  tags << tag unless tags.include?(tag)
end
Run Code Online (Sandbox Code Playgroud)

选项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
Run Code Online (Sandbox Code Playgroud)

获得唯一索引后,尝试添加重复记录将引发ActiveRecord::RecordNotUnique错误.处理此问题超出了本问题的范围.查看这个问题.

rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
Run Code Online (Sandbox Code Playgroud)


spy*_*yle 25

另外上面的建议:

  1. 添加:uniqhas_and_belongs_to_many关联
  2. 在连接表上添加唯一索引

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

post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
Run Code Online (Sandbox Code Playgroud)

  • 有趣的是,[has_many关联文档](http://guides.rubyonrails.org/association_basics.html)(搜索include?)建议不要因为竞争条件而使用`unless ... include?()`.我猜这同样适用于has_and_belongs_to_many. (6认同)
  • 清洁解决方案 在你的`Post`模型中,添加`def tag =(tag); tags << tag除非tags.include?(tag); 结束`坚固性. (3认同)

cyr*_*ier 21

在Rails4中:

class Post < ActiveRecord::Base 
  has_and_belongs_to_many :tags, -> { uniq }
Run Code Online (Sandbox Code Playgroud)

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

Rails文档

  • Downvoted,因为这显然**不会避免表中的重复** (7认同)
  • ` - > {uniq}`范围不会阻止重复数据,但有助于在视图中只显示一个. (5认同)

Sim*_*tti 20

您可以按照文档中的说明传递:uniq选项.另请注意,这些选项不会阻止创建重复关系,它只会确保访问者/查找方法会选择一次.:uniq

如果要防止关联表中出现重复,则应创建唯一索引并处理异常.此外,validates_uniqueness_of无法按预期工作,因为您可以考虑第二个请求在第一个请求检查重复项和写入数据库之间写入数据库的情况.

  • 注意:我已经有了唯一的约束,导致异常,处理它们是一个巨大的痛苦. (2认同)

Jos*_*eek 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
Run Code Online (Sandbox Code Playgroud)

  • 我认为uniq会在读取时忽略重复但仍然允许您编写重复项,如果您在数据库层有重复约束,则会引发数据库异常 (14认同)
  • 这不能按预期工作.尝试通过<<添加它们时,仍然会出现重复的约束错误. (4认同)
  • 这不会阻止重复,它只会隐藏它们不显示 (3认同)

Jef*_*ire 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
Run Code Online (Sandbox Code Playgroud)

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

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


Sam*_*ron 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
Run Code Online (Sandbox Code Playgroud)

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