web-dev-qa-db-ja.com

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 

これで、タグテーブルに大量の重複が表示されることを除いて、すべて正常に機能します。

タグテーブルの重複(名前に基づく)を回避するために何をする必要がありますか?

57
Sam Saffron

私はこれを回避するために、問題を修正する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

タグを使用してバッチで動作するようにわずかに最適化することもできますが、わずかに優れたトランザクションサポートが必要になる場合もあります。

4
Sam Saffron

ビューのみで重複を防ぐ(遅延ソリューション)

次のは、データベースへの重複する関係の書き込みを防止しません。これは、findメソッドが重複を無視することを保証するだけです。

In Rails 5:

_has_and_belongs_to_many :tags, -> { distinct }
_

注:_Relation#uniq_はRails 5( commit

In 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_エラーが発生します。これを処理することは、この質問の範囲外です。表示 これSO質問

_rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
_
42
Jeremy Lynch

さらに、上記の提案:

  1. 追加 :uniqからhas_and_belongs_to_many関連付け
  2. 結合テーブルに一意のインデックスを追加する

私は、関係がすでに存在するかどうかを判断するために明示的なチェックを行います。例えば:

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

Rails4の場合:

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

(注意、-> { uniq }は、リレーション名の直後、他のパラメータの前にある必要があります)

Railsのドキュメント

21
cyrilchampier

:uniqオプションとして ドキュメントに記載 。また、:uniqオプションは、重複する関係の作成を防止しません。それは、アクセサ/検索メソッドがそれらを一度だけ選択することを保証するだけです。

関連テーブルの重複を防ぎたい場合は、一意のインデックスを作成して例外を処理する必要があります。また、validates_uniqueness_ofは期待どおりに動作しません。これは、最初のリクエストが重複をチェックしてからデータベースに書き込むまでの間に、2番目のリクエストがデータベースに書き込んでいるケースに陥る可能性があるためです。

20
Simone Carletti

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
13
Joshua Cheek

このようにモデルを調整してクラスを作成したいと思います。

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

次に、作成をロジックでラップして、タグモデルがすでに存在する場合は再利用できるようにします。タグ名に一意の制約を課して、それを強制することもできます。結合テーブルのインデックスを使用するだけで特定のタグのすべての投稿と特定の投稿のすべてのタグを見つけることができるため、どちらの方法でもより効率的に検索できます。

唯一の問題は、タグ名を変更するとそのタグのすべての使用に影響するため、タグの名前を変更できないことです。ユーザーにタグを削除させ、代わりに新しいタグを作成させます。

5
Jeff Whitmire

これは本当に古いですが、自分のやり方を共有したいと思いました。

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

これには、必要に応じてタグを自動的に追加/削除するか、そうであれば何もしないという効果があります。

2
Javeed

私に働く

  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
    

セキュリティのためにタグ名を抽出します。タグテーブルにタグが存在するかどうかを確認し、存在しない場合は作成します。

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

次に、それがこの特定のコレクション内に存在するかどうかを確認し、存在しない場合はプッシュします。

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

Tag:nameプロパティにインデックスを追加し、Tags#createメソッドでfind_or_createメソッドを使用する必要があります

ドキュメント

0
ajbraus

レコードを追加する前に、コントローラーにチェックを追加するだけです。含まれている場合は何もせず、含まれていない場合は新しいものを追加します。

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

0
Matthew Bennett