LoginSignup
152
164

More than 5 years have passed since last update.

railsで多対多な関係を実装する時のポイント(加筆修正するかも)

Posted at

横断的にな資料が意外となかったりした。

実現したいこと

投稿とタグとみたいな関係。投稿は0〜複数のタグをもつ一方、タグも0〜複数の投稿に関連付けられている。とする。

例えば
1. 「もっと評価されるべきかつおだしの魅力」 という投稿には 「もっと評価されるべき」と「かつおだし」といった投稿に関連するタグが付いている。
2. 一方、「かつおだし」というタグから見ると「もっと評価されるべきかつおだしの魅力」や「かつおだしと白菜でミルフィーユ鍋作ってみた」などの、「かつおだし」に関連する投稿に付いている。

こういう関係をRuby on Rails上で再現したかった・・・、たどり着くまでが面倒。分かれば納得。

環境

  • Ruby on Rails 4.2.5
  • SQLite 3

ポイント

  1. 投稿とタグの間に、両者に関連づいた中間モデル(テーブル)を用意する
  2. 投稿、タグ、中間の各モデルファイルに対し、関連性を明記する
  3. ビュー部分は collection_check_boxes を使う
  4. Strong Parameter の修正も必要

 実装

1:投稿とタグの間に、両者に関連づいた中間モデル(テーブル)を用意する

いつものアレ(bundle使ってることを前提 不要な人は bundle exec はスルーしてください )

投稿に関するモデルを生成

名前は post とする

  • name タイトル
  • mode 表示モード(にする予定)
  • desc 本文(string じゃなくて text のほうがよかったか?)
terminal
$ bundle exec rails generate scaffold post name:string mode:integer desc:string

タグに関するモデルを生成する

名前は tag とする

  • label タグ名 1行テキストでいいか。
terminal
$ bundle exec rails generate scaffold tag name:string mode:integer desc:string

投稿とタグの関連付け

名前は post_tag とする

  • post 投稿idを保存する
  • tag タグidを保存する
terminal
$ bundle exec rails generate model post_tag post:references tag:references

ここでのポイントは、 各カラムにreferences という指定をしていること。
Railsドキュメントによると、

他のテーブルへの外部キーを表すカラムを生成

とのこと。実際にこのコマンドで生成されるマイグレーションファイルは
下記みたいになります。

migrate_createposttags
class CreatePostTags < ActiveRecord::Migration
  def change
    create_table :post_tags do |t|
      t.references :post, index: true, foreign_key: true
      t.references :tag, index: true, foreign_key: true
      t.timestamps null: false
    end
  end
end

投稿、タグ、中間の各モデルファイルに対し、関連性を明記する

Railsの仕様で、モデル同士の関連付けは各modelファイルに記述することになってます。ここでは以下のようにします。

  1. 投稿は複数のタグと関連付けられる
  2. タグは複数の投稿と関連付けられる
  3. 関連付けのレコードは、投稿かタグのどちらかが削除されたときに消去されること。

特に、3番目の関連付けの削除は見落としやすいので注意。

投稿(post)のモデル記述

(関係あるところだけ抜粋)

app/model/post.rb
class Post < ActiveRecord::Base
    has_many :post_tags, dependent: :destroy
    has_many :tags, :through => :post_tags

    accepts_nested_attributes_for :post_tags, allow_destroy: true
end

細かい解説

has_many :post_tags , dependent: :destroy
post(投稿)は複数のpost_tag(投稿関連付け)を持つ。 
従属関係があり、postが破棄されたとき関連するpost_tagも破棄される

has_many :tags ,:through => post_tags
postは複数のtag(タグ)を持つ。post_tags を通して。

accepts_nested_attributes_for :post_tags, allow_destroy: true

関連項目も含めて一度に保存、削除するよ。という意味。

この辺りも参考。

http://qiita.com/hmuronaka/items/818c421dc632e3efb7a6
http://o.inchiki.jp/obbr/81

タグ(tag)のモデル記述

(関係あるところだけ抜粋)

1つのタグがpost_tagを複数持っていること、
post_tagを通して複数のpost に紐付いていることを記述。

タグの方からどのpostを関連付けるか・・・という操作は、現時点では考えていないので、accepsts_nested_attributes_for は記述しない

app/model/tag.rb
class Tag < ActiveRecord::Base

    has_many :post_tags, dependent: :destroy
    has_many :posts, :through => :post_tags

end

細かい説明は省略。

投稿とタグの関連付け(post_tag)のモデル記述

(関係あるところだけ抜粋)

post_tagに関しては、postとtagの関連付けにしか使っていないので、
postとtag双方に属することだけ記述する。

app/model/post_tag.rb
class PostTag < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag
end

belongs_to に関しての解説はこちら

ビュー、コントローラに関連付けを記載する。

ビューの改修

(関係あるところだけ抜粋)

app/views/post/view.html.erb
<%= form_for(@post) do |f| %>
 (中略)
  <div class="field">
     <%= f.label :tags %><br>
    <%= collection_check_boxes(:post, :tag_ids, Tag.all, :id, :label ) do |t|  %>
      <% t.label { t.check_box + t.text } %>
    <% end %> 
  </div>
 以下略
<% end %>
抜粋
collection_check_boxes(:post, :tag_ids, Tag.all, :id, :label ) do |t|

これ何やっているのかというと・・・apidocs読んでもいまひとつわかりにくい。

  • 第一引数:オブジェクト名。ここではpost(投稿)を対象にしている。
  • 第二引数:メソッド名。ここではpost_tagのtag_idに対する値をセットしたいので、tag_ids とした(複数形で記述すること!!)
  • 第三引数:コレクション:どうも第四引数や第五引数で引用するものを撮ってきているようだ。ここではtagに属するすべてのデータを取得しておく
  • 第四引数:バリューメソッド:要はタグのvalue 部分に入る値を指定しているようだ。
  • 第五引数:テキストメソッド:要はタグの

表示されるhtmlソース見た方がわかりやすい

出力抜粋
<label for="post_tag_ids_1"><input type="checkbox" value="1" name="post[tag_ids][]" id="post_tag_ids_1" />登録たぐ1</label>

要は・・・Tagの値を取得して、valueに対しtag_idを、説明部分にlabelを入れていることになります。

※実際に実装して動かしてみた方がわかりやすいです。だれかうまい解説求む。

コントローラの改修

StrongParameterに、tag_idsの読み込みを許可させる。(重要)

app/controllers/posts_contoroller.rb
    def post_params
      params.require(:post).permit(:name, :mode, :desc, { :tag_ids=> [] })
    end

ここでのポイント

before
params.require(:post).permit(:name, :mode, :desc)
after
params.require(:post).permit(:name, :mode, :desc, { :tag_ids=> [] })

改修後には、tag_ids の 配列タイプを許可してます。

サーバログから何が起こってるのかを見る

手元のターミナルに出力されているログから、何が行われたかを確認する。

パラメータが送られる
Parameters: {"utf8"=>"✓", "authenticity_token"=>"NMDmN/AjqAD5AiWIPaxTNhYhpHsHRUlVcY/YQx6n0WHfjZKsOb7b460PXTZM38VOE97q47+Ez7PK4KPwmvMAXg==", "post"=>{"name"=>"テスト登録2", "mode"=>"1", "desc"=>"テストです", "tag_ids"=>["1", "2", ""]}, "commit"=>"Create Post"}

さきほどのフォームで作成されたtag_idが配列形式で用意されています。

DB処理
   (0.2ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "posts" ("name", "mode", "desc", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "テスト登録2"], ["mode", 1], ["desc", "テストです"], ["created_at", "2016-01-10 10:20:47.163133"], ["updated_at", "2016-01-10 10:20:47.163133"]]
  SQL (0.1ms)  INSERT INTO "post_tags" ("tag_id", "post_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["tag_id", 1], ["post_id", 3], ["created_at", "2016-01-10 10:20:47.164983"], ["updated_at", "2016-01-10 10:20:47.164983"]]
  SQL (0.0ms)  INSERT INTO "post_tags" ("tag_id", "post_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["tag_id", 2], ["post_id", 3], ["created_at", "2016-01-10 10:20:47.166045"], ["updated_at", "2016-01-10 10:20:47.166045"]]
   (2.8ms)  commit transaction

post(投稿)と、post_tag(投稿とタグの関連付け)にカラムが作成されています。

152
164
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
152
164