Webとデザインのあれこれ

プログラミングとUIデザインの学習記録です。

Railsの学習(アソシエーション編)

ポリモーフィック関連付けを理解しよう

本日の記事について

現在、フィヨルドブートキャンプでプログラミングを学習中です。本記事では、スクールでの課題を進める上での気づきなどをまとめています。

まずアプリ作成の流れなどを振り返ります。

  1. Railsの教科書』内で紹介されていたBookアプリを作成します
  2. 日本語に対応するためにgemi18nを実装
  3. ページネーション機能をgemkaminariで実装
  4. ログイン・ログアウト機能をgemdevisedevise-i18nで実装
  5. GitHubによるログイン機能をgemOmniAuthで実装 
  6. ActiveStorageによるプロフィール画像のアップロード機能実装
  7. コメント機能を実装してみよう!←今日はここです!

現状では、以下の3つのモデルが存在しています。

  • 書籍の登録情報を記録するBookモデル
  • 日報のデータを記録するReportモデル
  • ユーザー情報を記録するUserモデル

reportsとそのCRUD機能については、booksとほぼ同様ですので、作成手順などを記事では紹介していません。

コメント機能実装の目的と手順

実装の目的は以下の2つです。

  • コメント機能をbooksreportsに追加する
  • ポリモーフィック関連付けを用いて、booksおよびreportsにつけたコメントを共通のコードで動かす

実装のざっくりした手順は以下となります。

  1. commentsのモデル(Comment)とcontroller(comments_controller.rb)を生成する
  2. モデル生成時に、t.referenceの記述を入れてmigrateする
  3. models/のファイルやconfig/routes.rbにルーティングを記述する
  4. viewsとcontrollerの処理などを独自で記述する

手順の詳解

1. commentsのモデル(Comment)とcontroller(comments_controller.rb)を生成する

まず、Commentのモデル作りましょう。

rails g model Comment title:string body:text 

マイグレーションスクリプトを編集します。

class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :title, null: false
      t.text :body, null:false
      t.references :commentable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

新たにモデルを作成した際の通常のマイグレーションスクリプトとは少しだけ内容が異なっているのがお分かり頂けますでしょうか。

実は、t.references :commentable, polymorphic: true, index: trueという記述を追記しています。

こちらは何を意味しているのでしょうね?この内容を理解するために、まずポリモーフィックについて確認する必要があります。

ポリモーフィック関連付けとは

少し触れましたが、本課題では、booksおよびreportsにつけたコメントを共通のコードで動かすことが条件となっています。

よく考えるとすごく難しそうですよね。何故なら、CommentはBookにもReportにも属していることになるからです。どのように関連付けしたら良いのか不明ですね。

このような場合に役立つのがポリモーフィック関連付けです。

こちらを用いると、ある1つのモデル(Comment)が他の複数のモデル(BookReport)に属していることを、1つの関連付けだけで表現することが可能になります。

f:id:b_leiu:20190611150243p:plain

# 記載方法1
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :title, null: false
      t.text :body, null:false
      t.integer :commentable_id
      t.string  :commentable_type
      t.timestamps
    end
      add_index :comments, [:commentable_type, :commentable_id]
  end
end

上記のER図の通り、commentable_idcommentable_typeは外部参照キーとなります。必ずカラムの型を宣言するのを忘れないようにしましょう。

そして、app/models/の各ファイルに以下を記述します。

2. モデル生成時に、t.referenceの記述を入れてmigrateする

3. models/のファイルやconfig/routes.rbにルーティングを記述する

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

# app/models/book.rb
class Book < ApplicationRecord
  has_many :comments, as: :commentable
end

# app/models/report.rb
class Report < ApplicationRecord
  has_many :comments, as: :commentable
end

このように宣言してあげることにより、commentableで関連付けが可能になります。

ルーティングの設定も忘れないようにしましょう。

# config/routes.rb
resources :books do
  resources :comments, only: [:show, :create, :destroy, :edit, :update]
end

resources :reports do
  resources :comments, only: [:show, :create, :destroy, :edit, :update]
end

# only: [:show, :create, :destroy, :edit, :update]は適宜

こうすることで、 下記のようにパスを返すメソッドが設定されます。

例: report_comment_path(パスは/reports/:report_id/comments/:id)


関連付けできているか見てみよう!

注意:この画面は一通り実装が終わった後の画面です。

f:id:b_leiu:20190611154553p:plain

rails console(rails c)でテーブルの情報を実際に確認しましょう!

$ comment = Comment.find(1)

f:id:b_leiu:20190611155055p:plain

外部キーのcommentable_typeに親モデルのクラスであるBookが入っていることが確認できますね。また、commentable_id にはBookのidが入っています。

では、models/以下のファイルマイグレーションスクリプトに記載したcommentableで親モデルの情報を取得しましょう。

$ comment.commentable

f:id:b_leiu:20190611155839p:plain

コメントに紐づいたBookモデルのインスタンスが確認できましたね!関連付け、成功です。

ちなみに、t.referencesを用いるとマイグレーションスクリプトの記述内容を減らすことができます。簡略化した記載方法ですので、記載方法1と共に参考にしてください。

# 記載方法2
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.string :title, null: false
      t.text :body, null:false
      t.references :commentable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

buildの利用について

ここまでは主に関連付けの方法について確認しました。ここからは、処理を記述する際に悩みやすいポイントについて整理します。

まず、日報(report)に紐づいたコメント(comment)を新規作成する処理について考えましょう。

それぞれのモデルである、インスタンス変数@reportと@commentを利用します。 コントローラの処理内で@commentを親モデルの@reportに紐づけるにはどうしたらよいのでしょうか。 このような場合はbuildを用いると比較的楽に実装できます。

# comments_controller.rb

@report = Report.find(params[:report_id])
@comment = @report.comments.build 

上記のように記述してあげると、外部参照キーなどの設定をしなくても自動で紐づきが可能となります。

あとは、フォームを記述すればOKです。

# show.html.erb

<%= form_with(model: [@report,@comment], local: true)do |f| %>
      <%= f.label :content  %>
      <%= f.text_area :content  %>
      <%= f.submit "コメントする" %>
  <% end %>

処理の詳細については、割愛いたします。

最後に

いかがでしたでしょうか。関連付け、とても奥が深いですね!

参考文献:

www.sejuku.net

qiita.com

railsguides.jp

ruby-rails.hatenadiary.com