Ruby-on-rails has_and_belongs_to_many,避免连接表中的重复

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1129781/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 21:28:16  来源:igfitidea点击:

has_and_belongs_to_many, avoiding dupes in the join table

ruby-on-railsactiverecord

提问by Sam Saffron

I have a pretty simple HABTM set of models

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

Now it all works alright except that I get a ton of duplicates in the Tags table.

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

What do I need to do to avoid duplicates (bases on name) in the tags table?

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

采纳答案by Sam Saffron

I worked around this by creating a before_save filter that fixes stuff up.

我通过创建一个 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

It could be slightly optimised to work in batches with the tags, also it may need some slightly better transactional support.

它可以稍微优化以批量处理标签,也可能需要一些稍微更好的事务支持。

回答by Jeremy Lynch

Prevent duplicates in the view only(Lazy solution)

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

The following does notprevent writing duplicate relationships to the database, it only ensures findmethods ignore duplicates.

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

In Rails 5:

在 Rails 5 中:

has_and_belongs_to_many :tags, -> { distinct }

Note: Relation#uniqwas depreciated in Rails 5 (commit)

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

In Rails 4

在 Rails 4

has_and_belongs_to_many :tags, -> { uniq }


Prevent duplicate data from being saved(best solution)

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

Option 1:Prevent duplicates from the controller:

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

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

However, multiple users could attempt post.tags.include?(tag)at the same time, thus this is subject to race conditions. This is discussed here.

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

For robustness you can also add this to the Post model (post.rb)

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

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

Option 2:Create a unique index

选项 2:创建唯一索引

The most foolproof way of preventing duplicatesis to have duplicate constraints at the database layer. This can be achieved by adding a unique indexon the table itself.

防止重复的最万无一失的方法是在数据库层设置重复约束。这可以通过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

Once you have the unique index, attempting to add a duplicate record will raise an ActiveRecord::RecordNotUniqueerror. Handling this is out of the scope of this question. View this SO question.

一旦拥有唯一索引,尝试添加重复记录将引发ActiveRecord::RecordNotUnique错误。处理这个超出了这个问题的范围。查看此问题

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

回答by spyle

In addition the suggestions above:

另外还有上面的建议:

  1. add :uniqto the has_and_belongs_to_manyassociation
  2. adding unique index on the join table
  1. 添加:uniqhas_and_belongs_to_many协会
  2. 在连接表上添加唯一索引

I would do an explicit check to determine if the relationship already exists. For instance:

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

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

回答by cyrilchampier

In Rails4:

在 Rails4 中:

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

(beware, the -> { uniq }must be directly after the relation name, before other params)

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

Rails documentation

Rails 文档

回答by Simone Carletti

You can pass the :uniqoption as described in the documentation. Also note that the :uniqoptions doesn't prevent the creation of duplicate relationships, it only ensures accessor/find methods will select them once.

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

If you want to prevent duplicates in the association table you should create an unique index and handle the exception. Also validates_uniqueness_of doesn't work as expected because you can fall into the case a second request is writing to the database between the time the first request checks for duplicates and writes into the database.

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

回答by Joshua Cheek

Set the uniq option:

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

回答by Jeff Whitmire

I would prefer to adjust the model and create the classes this way:

我更愿意以这种方式调整模型并创建类:

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

Then I would wrap the creation in logic so that Tag models were reused if it existed already. I'd probably even put a unique constraint on the tag name to enforce it. That makes it more efficient to search either way since you can just use the indexes on the join table (to find all posts for a particular tag, and all tags for a particular post).

然后我会将创建包装在逻辑中,以便标签模型如果已经存在就可以被重用。我什至可能会在标签名称上设置一个唯一的约束来强制执行它。这使得以任何一种方式搜索都更有效,因为您可以只使用连接表上的索引(查找特定标签的所有帖子,以及特定帖子的所有标签)。

The only catch is that you can't allow renaming of tags since changing the tag name would affect all uses of that tag. Make the user delete the tag and create a new one instead.

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

回答by Jose Fuentes Delgado

To me work

给我工作

  1. adding unique index on the join table
  2. override << method in the relation

    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. 覆盖关系中的 << 方法

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

回答by Javeed

This is really old but I thought I'd share my way of doing this.

这真的很旧,但我想我会分享我的做法。

class Tag < ActiveRecord::Base 
    has_and_belongs_to_many :posts
end 

class Post < ActiveRecord::Base 
    has_and_belongs_to_many :tags
end

In the code where I need to add tags to a post, I do something like:

在需要向帖子添加标签的代码中,我执行以下操作:

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

This has the effect of automatically adding/removing tags as necessary or doing nothing if that's the case.

这具有根据需要自动添加/删除标签或在这种情况下什么都不做的效果。

回答by dav1dhunt

Extract the tag name for security. Check whether or not the tag exists in your tags table, then create it if it doesn't:

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

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

Then check whether it exists within this specific collection, and push it if it doesn't:

然后检查它是否存在于这个特定的集合中,如果不存在则推送它:

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