[解決済み] railsで同じモデルで多対多の関係?
質問
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_connections
に
posts.id = post_connections.post_a_id
.
2つ目の関連付けでは、最初の関連付けを通じて、この投稿に接続されている他の投稿に到達できることをRailsに伝えています。
post_connections
を経由して、その後に続く
post_b
の関連付けは
PostConnection
.
ちょうど
もうひとつ
をRailsに伝える必要があります。
PostConnection
が所属する投稿に依存していることを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
で、これはより高速ですが、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_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 )
この関連付けを双方向にするためには、Railsの
OR
で上記の条件を
post_a_id
と
post_b_id
を反転させると、両方向に見えるようになります。
残念ながら、私が知る限りこれを行う唯一の方法は、かなり面倒なものです。のオプションを使って手動でSQLを指定しなければなりません。
has_and_belongs_to_many
のような
:finder_sql
,
:delete_sql
など。きれいなものではありません。(こちらも提案を受け付けます。どなたか)
関連
-
[解決済み] pg gem をインストールしようとすると 'libpq-fe.h' ヘッダが見つからない
-
[解決済み] RVMでRubyのデフォルトバージョンを設定するには?
-
[解決済み] バリデーションなしで属性を更新する方法
-
[解決済み] Rubyでnilとemptyとblankを理解する方法
-
[解決済み] Ruby on Railsで現在の絶対URLを取得するにはどうすればよいですか?
-
[解決済み] エラーが発生しました。pgsqlをrailsで動作させようとすると、Peer authentication failed for user "postgres" と表示されます。
-
[解決済み] Rails 4で懸念事項を使用する方法
-
[解決済み】Railsの認証トークンを理解する
-
[解決済み】Ruby on Railsはモデルのfield:typeを生成します - field:typeのオプションは何ですか?
-
[解決済み】Rails: モデルがすでに存在するときに`rails generate scaffold`を実行するにはどうすればよいですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】Rails 4 RoutingError: ルートが一致しない[POST]。
-
[解決済み] 新規ユーザー作成時に ActiveModel::ForbiddenAttributesError が発生する。
-
[解決済み] erbでコメントを追加する最適な方法
-
[解決済み] Errno::EACCESS: パーミッションが拒否された @ dir_s_mkdir
-
[解決済み] 検索 vs 発見する by vs どこに
-
[解決済み] Rails ExecJS::ProgramError in Pages#home?
-
[解決済み] nil:NilClass の未定義メソッド `each'... なぜ?
-
[解決済み] RoRにおけるSpringサーバーの機能とは?
-
[解決済み] railsで':remote => true'はどのように動作するのでしょうか?
-
[解決済み] 属性とカラムの違いは何ですか?