1. ホーム
  2. ruby-on-rails

[解決済み] railsで同じモデルで多対多の関係?

2022-09-25 16:51:18

質問

railsで同じモデルで多対多のリレーションを作るにはどうしたらよいでしょうか?

例えば、各投稿が多くの投稿とつながっているような場合です。

どのように解決するのですか?

多対多の関係にはいくつかの種類があります。

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

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

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

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

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

まずはこの基本的なスキーマを投稿に使ってみます。

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 . 大きな声で文句を言われるでしょう。

最後に、カラムの名前も非標準であることに注意してください (not post_id でない)、競合を防ぐためです。

さて、モデルの中では、これらの2つの非標準的な事柄を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_many , belongs_to , has_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 から推論することができないので、これは必要です。 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つ目の関連付けでは、最初の関連付けを通じて、この投稿に接続されている他の投稿に到達できることをRailsに伝えています。 post_connections を経由して、その後に続く post_b の関連付けは PostConnection .

ちょうど もうひとつ をRailsに伝える必要があります。 PostConnection が所属する投稿に依存していることをRailsに伝える必要があることです。もし片方または両方の post_a_idpost_b_idNULL であった場合、その接続は私たちに多くを語らないでしょう?ここでは、その方法を 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 で、これはより高速ですが、destroyフックを使っている場合はそれを呼び出すことはできません。
  • を追加しました。 has_many の関連付けを を通して私たちをつないだものです。 post_b_id . こうすることで、Railsはそれらもきちんと破棄することができます。ただし :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_connectionsreverse_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 )

この関連付けを双方向にするためには、Railsの OR で上記の条件を post_a_idpost_b_id を反転させると、両方向に見えるようになります。

残念ながら、私が知る限りこれを行う唯一の方法は、かなり面倒なものです。のオプションを使って手動でSQLを指定しなければなりません。 has_and_belongs_to_many のような :finder_sql , :delete_sql など。きれいなものではありません。(こちらも提案を受け付けます。どなたか)