レール内の同じモデルとの多対多の関係ですか?


107

レール内の同じモデルと多対多の関係を作成するにはどうすればよいですか?

たとえば、各投稿は多くの投稿に関連付けられています。

回答:


276

多対多の関係にはいくつかの種類があります。次の質問を自問する必要があります。

  • 関連付けで追加情報を保存しますか?(結合テーブルの追加フィールド。)
  • 関連付けは暗黙的に双方向である必要がありますか?(投稿Aが投稿Bに接続されている場合、投稿Bも投稿Aに接続されています。)

これには、4つの異なる可能性があります。以下でこれらについて説明します。

参考までに、この件に関するRailsのドキュメント。「多対多」というセクションがあり、もちろんクラスメソッド自体に関するドキュメントがあります。

最も単純なシナリオ、単方向、追加フィールドなし

これはコードの中で最もコンパクトです。

私はあなたの投稿のためのこの基本的なスキーマから始めます:

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

多対多の関係では、結合テーブルが必要です。これがそのスキーマです:

create_table "post_connections", :force => true, :id => false do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
end

デフォルトでは、Railsはこのテーブルを、結合する2つのテーブルの名前の組み合わせと呼びます。しかし、それはposts_postsこの状況のように判明するので、post_connections代わりに採用することにしました。

ここで非常に重要なのは:id => false、デフォルトのid列を省略することです。Railsは、の結合テーブルを除くすべての場所でその列を必要としていますhas_and_belongs_to_many。大声で不平を言うでしょう。

最後に、post_id競合を防ぐために、列名も標準ではない(ではない)ことに注意してください。

モデル内で、これらの非標準の事項についてRailsに伝える必要があります。次のようになります。

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")
end

そして、それは単にうまくいくはずです!実行されたirbセッションの例を次に示しますscript/console

>> a = Post.create :name => 'First post!'
=> #<Post id: 1, name: "First post!">
>> b = Post.create :name => 'Second post?'
=> #<Post id: 2, name: "Second post?">
>> c = Post.create :name => 'Definitely the third post.'
=> #<Post id: 3, name: "Definitely the third post.">
>> a.posts = [b, c]
=> [#<Post id: 2, name: "Second post?">, #<Post id: 3, name: "Definitely the third post.">]
>> b.posts
=> []
>> b.posts = [a]
=> [#<Post id: 1, name: "First post!">]

posts関連付けに割り当てると、post_connections必要に応じてテーブルにレコードが作成されます。

注意すべき点:

  • 上記のirbセッションでは、関連付けが単方向であることを確認できます。これはa.posts = [b, c]、の出力にb.postsは最初の投稿が含まれないためです。
  • あなたが気づいたかもしれないもう一つのことは、モデルがないということですPostConnection。通常、has_and_belongs_to_many関連付けにはモデルを使用しません。このため、追加のフィールドにアクセスすることはできません。

単方向、追加フィールドあり

さて、今...ウナギの美味しさについて今日あなたのサイトに投稿した一般ユーザーがいます。この完全な見知らぬ人があなたのサイトにやって来て、サインアップし、通常のユーザーの不適当さに叱責の投稿を書きます。結局、うなぎは絶滅危惧種です!

そのため、データベースで、投稿Bが投稿Aの怒りの言葉であることを明確にしたいと思います。そのためにはcategory、関連付けにフィールドを追加する必要があります。

必要なのは、もはやありませんhas_and_belongs_to_manyが、の組み合わせhas_manybelongs_tohas_many ..., :through => ...およびのための余分なモデルがテーブルに参加します。この追加のモデルにより、関連付け自体に追加情報を追加することができます。

上記と非常によく似た別のスキーマを次に示します。

create_table "posts", :force => true do |t|
  t.string  "name", :null => false
end

create_table "post_connections", :force => true do |t|
  t.integer "post_a_id", :null => false
  t.integer "post_b_id", :null => false
  t.string  "category"
end

この状況で、列post_connections があることに注意してくださいid。(パラメーターはありません :id => false。)テーブルにアクセスするための通常のActiveRecordモデルがあるため、これは必須です。

それはPostConnection非常に単純なので、モデルから始めます。

class PostConnection < ActiveRecord::Base
  belongs_to :post_a, :class_name => :Post
  belongs_to :post_b, :class_name => :Post
end

ここで行われているのはのみです:class_name。これは、Railsがここから投稿を処理していること、post_aまたはpost_bそれを処理していることを推測できないためです。それを明示的に伝えなければなりません。

Postモデル:

class Post < ActiveRecord::Base
  has_many :post_connections, :foreign_key => :post_a_id
  has_many :posts, :through => :post_connections, :source => :post_b
end

最初のhas_many関連付けで、モデルにに参加するように指示post_connectionsposts.id = post_connections.post_a_idます。

2番目の関連付けでは、最初の関連付けpost_connections、続いてのpost_b関連付けを介して、他の投稿(この投稿に接続されている投稿)に到達できることをRailsに伝えていますPostConnection

不足していることがもう1つありPostConnectionます。それは、a が属する投稿に依存していることをRailsに通知する必要があるということです。一方または両方の場合にpost_a_idしてpost_b_idいたNULL場合、接続はあまり教えて、それだろうではないだろうか?Postモデルでこれを行う方法は次のとおりです。

class Post < ActiveRecord::Base
  has_many(:post_connections, :foreign_key => :post_a_id, :dependent => :destroy)
  has_many(:reverse_post_connections, :class_name => :PostConnection,
      :foreign_key => :post_b_id, :dependent => :destroy)

  has_many :posts, :through => :post_connections, :source => :post_b
end

構文のわずかな変更に加えて、ここでは実際の2つの点が異なります。

  • has_many :post_connections余分ている:dependentパラメータを。値:destroyを使用して、この投稿が消えると、先に進み、これらのオブジェクトを破棄できることをRailsに伝えます。ここで使用できる代替値はです:delete_all。これはより高速ですが、使用している場合は破壊フックを呼び出しません。
  • リバース接続のhas_many関連付けも追加しました。これは、を介してリンクされたものです。このようにして、Railsはそれらもきちんと破壊できます。モデルのクラス名はから推測できないため、ここで指定する必要があることに注意してください。post_b_id:class_name:reverse_post_connections

これで、次のirbセッションが始まりますscript/console

>> a = Post.create :name => 'Eels are delicious!'
=> #<Post id: 16, name: "Eels are delicious!">
>> b = Post.create :name => 'You insensitive cloth!'
=> #<Post id: 17, name: "You insensitive cloth!">
>> b.posts = [a]
=> [#<Post id: 16, name: "Eels are delicious!">]
>> b.post_connections
=> [#<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>]
>> connection = b.post_connections[0]
=> #<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>
>> connection.category = "scolding"
=> "scolding"
>> connection.save!
=> true

関連付けを作成してからカテゴリを個別に設定する代わりに、PostConnectionを作成してそれで終了することもできます。

>> b.posts = []
=> []
>> PostConnection.create(
?>   :post_a => b, :post_b => a,
?>   :category => "scolding"
>> )
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> b.posts(true)  # 'true' means force a reload
=> [#<Post id: 16, name: "Eels are delicious!">]

post_connectionsとのreverse_post_connections関連付けを操作することもできます。それはposts協会にきちんと反映されます:

>> a.reverse_post_connections
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> a.reverse_post_connections = []
=> []
>> b.posts(true)  # 'true' means force a reload
=> []

双方向ループ関連

通常のhas_and_belongs_to_many関連付けでは、関連付けは関係する両方のモデルで定義されます。関連付けは双方向です。

ただし、この場合、Postモデルは1つしかありません。また、関連付けは一度だけ指定されます。これこそが、この特定のケースで、関連付けが単方向である理由です。

同じことがhas_many、結合テーブルのモデルと結合テーブルのモデルにも当てはまります。

これは、単にirbから関連付けにアクセスし、Railsがログファイルに生成するSQLを確認する場合に最もよく見られます。次のようなものが見つかります。

SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )

関連が双方向にするために、我々は、makeの方法を見つける必要があるだろうレールORと、上記の条件をpost_a_idし、post_b_id逆に、それは両方の方向になります。

残念ながら、私が知っているこれを行う唯一の方法はかなりハックです。手動でのオプションを使用して、SQLを指定する必要がありますhas_and_belongs_to_manyような:finder_sql:delete_sqlそれはかなりではないなど、。(私もここで提案を受け入れます。誰か?)


素敵なコメントをありがとう!:)さらに編集を加えました。具体的には:foreign_keyhas_many :throughは必要ありません。の非常に便利な:dependentパラメーターの使用方法についての説明を追加しましたhas_many
ステファンKochen

@Shtééfでも、大量の割り当て(update_attributes)は双方向の関連付けの場合は機能しません。例:postA.update_attributes({:post_b_ids => [2,3,4]})アイデアや回避策?
Lohith MV 2012

非常に素晴らしい回答メイト5.times {puts "+1"}
Rahul

@Shtééf私はこの答えから多くを学びました、ありがとう!:私が尋ねるとここにあなたの双方向関連の質問に答えることを試みたstackoverflow.com/questions/25493368/...
jbmilgromを

17

シュティーフが提起した質問に答えるには:

双方向ループ関連

ユーザー間のフォロワーとフォロワーの関係は、双方向ループ関連の良い例です。ユーザーは多くを持つことができます。

  • フォロワーとしての能力でフォロワー
  • フォロワーとしての能力でのフォロワー。

user.rbのコードはのようになります。

class User < ActiveRecord::Base
  # follower_follows "names" the Follow join table for accessing through the follower association
  has_many :follower_follows, foreign_key: :followee_id, class_name: "Follow" 
  # source: :follower matches with the belong_to :follower identification in the Follow model 
  has_many :followers, through: :follower_follows, source: :follower

  # followee_follows "names" the Follow join table for accessing through the followee association
  has_many :followee_follows, foreign_key: :follower_id, class_name: "Follow"    
  # source: :followee matches with the belong_to :followee identification in the Follow model   
  has_many :followees, through: :followee_follows, source: :followee
end

follow.rbのコードは次のとおりです。

class Follow < ActiveRecord::Base
  belongs_to :follower, foreign_key: "follower_id", class_name: "User"
  belongs_to :followee, foreign_key: "followee_id", class_name: "User"
end

注意すべき最も重要なことは、おそらく用語:follower_follows:followee_followsuser.rbにあります。ミルの実行(非ループ)の関連付けを例として使用する場合、チームには:playersからまでの数が多い場合があります:contracts。これは、(そのようなPlayerのキャリアの過程で)多くのスルーを持っている可能性のあるPlayerでも同じです。ただし、この場合、名前付きモデルが1つしか存在しない場合(つまり、User)、through:関係に同じ名前を付けると(たとえば、上記の投稿の例で行ったように)、(またはアクセスポイント)への結合テーブル。そして:teams:contractsthrough: :followthrough: :post_connections:follower_follows:followee_followsこのような名前の衝突を回避するために作成されました。今、ユーザーは多くを持つことができ:followers:follower_follows、多く:followees:followee_follows

判断するにはユーザーさん:フォロイー(時に@user.followeesデータベースへのコール)、Railsは今CLASS_NAMEの各インスタンスを見てもよい。そのようなユーザーがフォロワー(すなわちある『フォロー』foreign_key: :follower_idなど:を通じて)ユーザーさん:followee_followsを。判断するにはユーザーさん:フォロワー(時に@user.followersデータベースへのコール)、Railsは今CLASS_NAMEの各インスタンスを見てもよい:例えば、 『フォロー』ユーザーがフォロイー(すなわちあるforeign_key: :followee_idように:を介して)ユーザーさん:follower_followsを。


1
まさに私が必要としたもの!ありがとう!(データベースの移行も一覧表示することをお勧めします。承認された回答からその情報を収集する必要がありました)
Adam Denoon

6

Railsで友達関係を作成する方法を見つけるために誰かがここに来た場合、私は彼らに私が最終的に使用することに決めたもの、つまり「コミュニティエンジン」が行ったことをコピーすることを紹介します。

以下を参照できます。

https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb

そして

https://github.com/bborn/communityengine/blob/master/app/models/user.rb

詳細については。

TL; DR

# user.rb
has_many :friendships, :foreign_key => "user_id", :dependent => :destroy
has_many :occurances_as_friend, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy

..

# friendship.rb
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"

2

@StéphanKochenに触発され、これは双方向の関連付けで機能する可能性があります

class Post < ActiveRecord::Base
  has_and_belongs_to_many(:posts,
    :join_table => "post_connections",
    :foreign_key => "post_a_id",
    :association_foreign_key => "post_b_id")

  has_and_belongs_to_many(:reversed_posts,
    :class_name => Post,
    :join_table => "post_connections",
    :foreign_key => "post_b_id",
    :association_foreign_key => "post_a_id")
 end

その後、post.posts&& post.reversed_postsは両方とも機能し、少なくとも私にとっては機能するはずです。


1

双方向belongs_to_and_has_manyについては、すでに投稿されているすばらしい回答を参照してから、別の名前で別の関連付けを作成し、外部キーを逆にclass_nameして、正しいモデルを指すように設定していることを確認してください。乾杯。


2
あなたの投稿に例を示していただけませんか?あなたが提案したように私は複数の方法を試しましたが、それを打ち消すことができないようです。
achabacha322

0

誰かが仕事に対して優れた答えを得るのに問題を抱えていた場合:

(オブジェクトは#inspectをサポートしていません)
=>

または

NoMethodError::Mission:Symbolの未定義のメソッド `split '

次に、解決策はで置き換え:PostConnection"PostConnection"、もちろんクラス名を置き換えます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.