猫Rails

ねこー🐈

ポリモーフィック関連のコントローラー

まだまだRails勉強中の身なので、間違いもあるかと思います💦間違いを見つけた場合は、コメントいただけると嬉しいです🙇

お世話になっているFJORD BOOT CAMP(フィヨルドブートキャンプ)さんでこのような課題が出ました。

ポリモーフィック関連を使い、BookとReportにコメント機能をつけよ。(要約)

シンプルな課題ですが、難しいです。 ポリモーフィック関連の実装方法を知っているだけではだめで、以下のような知識も必要になります。

Commentのコントローラーを実装しながら、解説に挑戦してみます!

シンプルなコントローラー

まずはCommentのCRUDが欲しいので、CommentのScaffoldを作成します。

$ rails g scaffold Comment body

ルーティングとコントローラーはこんな感じです。

# config/routes.rb
resources :comments
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /comments
  def index
    @comments = Comment.all
  end

  # GET /comments/1
  def show
  end

  # GET /comments/new
  def new
    @comment = Comment.new
  end

  # GET /comments/1/edit
  def edit
  end

  # POST /comments
  def create
    @comment = Comment.new(comment_params)

    if @comment.save
      redirect_to @comment, notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /comments/1
  def update
    if @comment.update(comment_params)
      redirect_to @comment, notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /comments/1
  def destroy
    @comment.destroy
    redirect_to comments_url, notice: 'Comment was successfully destroyed.'
  end

  private
    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

よく見るコードですが、大事なコードです。 Scaffoldで作成されるコントローラーは、コントローラーの理想形だと思います。 できるだけこの形を壊さないように、修正していきます。

ネストしたリソースのコントローラー

少し遠回りになりますが、ポリモーフィック関連を実装する前にネストしたリソースを実装します。

ネストしたリソースというのはこれのことです。

resources :reports do
  resources :comments
end

通常のresources :commentsだとコメント一覧はGET /commentsです。

一方ネストしたリソースだとGET /reports/1/commentsになります。

URLでReportとCommentの親子関係を表現することで、id=1のreportに紐づくcomment一覧を取得できるようになります。

ポリモーフィック関連のリソースは必然的にネストしたリソースになります(たぶん)。 ポリモーフィック関連とネストしたリソースの2つを同時に実装すると、問題が起きた時に切り分けが難しくなるので、慣れないうちは分けて考えたほうがいいかと思います。

以下、ネストしたリソースのコードです。

モデル

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :report
end

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

ルーティング

resources :reports do
  resources :comments
end

コントローラー

class CommentsController < ApplicationController
  before_action :set_report
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /reports/1/comments
  def index
    @comments = @report.comments
  end

  # GET /reports/1/comments/1
  def show
  end

  # GET /reports/1/comments/new
  def new
    @comment = @report.comments.build
  end

  # GET /reports/1/comments/1/edit
  def edit
  end

  # POST /reports/1/comments
  def create
    @comment = @report.comments.build(comment_params)

    if @comment.save
      redirect_to [@report, @comment], notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /reports/1/comments/1
  def update
    if @comment.update(comment_params)
      redirect_to [@report, @comment], notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /reports/1/comments/1
  def destroy
    @comment.destroy
    redirect_to [@report, :comments], notice: 'Comment was successfully destroyed.'
  end

  private
    def set_report
      @report = Report.find(params[:report_id])
    end

    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

参考: https://github.com/amatsuda/nested_scaffold/blob/master/lib/generators/scaffold_controller/templates/controller.rb

ポリモーフィックなルーティング

上記のコードでは、createアクションのリダイレクト処理はこうなっています。

redirect_to [@report, @comment], notice: 'Comment was successfully created.'

[@report, @comment]の部分は、Rails内部でurl_for([@report, @comment])となり、/reports/1/comments/1というurlになります。

[@report, @comment]の部分はreport_comment_path(@report, @comment)という書き方もできます。 こっちの方がよく見る気がします。 しかしこっちの書き方だと、ポリモーフィック関連を使う際に問題が出ます。

後でポリモーフィック関連を実装する際に、reportだけでなくbookも扱えるようにするため、[@report, @comment]@reportの部分を@commentableに抽象化して[@commentable, @comment]にします。 この時@commentableの参照先はreportオブジェクトbookオブジェクトです。

Rails@commentableをいい感じに解釈して、@commentablereportオブジェクトの場合は/reports/1/comments/1というURLを作り、@commentablebookオブジェクトの場合は/books/1/comments/1というURLを作ってくれます。

このコード1つで2つのURLに対応してくれます。

ここでreport_comment_path(@report, @comment)と書いてしまうと、後に分岐処理を書くことになり、ポリモーフィック関連の強みを活かせません。 そのため[@report, @comment]という書き方を利用しています。

ちなみに[@report, @comment]のような書き方は、redirect_toだけでなくlink_toform_withでも利用できます。 この知識はビューを書く際に必要になります。

参考

ポリモーフィック関連のモデル

ポリモーフィック関連ポリモーフィック(polymorphic)とはオブジェクト指向の本などに出てくるポリモーフィズム(polymorphism)のことです。

Rubyの場合はダックタイピングの文脈で語られます。

そのためポリモーフィック関連を正しく使うためには、ポリモーフィズムダックタイピングの知識が必要になります。

自分はオブジェクト指向の知識が全くない状態でポリモーフィック関連を使い、悲惨な目にあいました😂

以下の2つの記事が素晴らしいのでおすすめです。

それではポリモーフィック関連を実装していきます。

素朴に実装するとこうなります。

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

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

参考: https://railsguides.jp/association_basics.html#%E3%83%9D%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91

これでも良いのですが、Commentableとしての責務をmoduleに切り出すとさらに良いです。 moduleに切り出すことで名前と境界が明確になり、ReportがCommentableとして振る舞えることを意識しやすくなります。

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

# app/models/report.rb
class Report < ApplicationRecord
  include Commentable
end

# app/models/concerns/commentable.rb
module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable
  end
end

moduleに切り出す際にActiveSupportのconcernという機能を使っています。

参考: https://qiita.com/castaneai/items/6dc121ce6ff100614f42

これでreport - commentsだった関連がcommentable - commentsとなり、抽象化できました。 Bookでinclude Commentableすることで、bookreportを同じcommentableとして扱えるようになります。

ポリモーフィック関連のコントローラー

ネストしたリソースのコントローラーを修正してポリモーフィック関連のコントローラーにします。

ルーティングはそのままです。

resources :reports do
  resources :comments
end

コントローラーはこうなります。

class CommentsController < ApplicationController
  before_action :set_commentable
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /reports/1/comments
  # reportsと同じようにbooksをルーティングに追加すれば、
  # GET /books/1/commentsも可能になります。
  def index
    @comments = @commentable.comments
  end

  # GET /reports/1/comments/1
  def show
  end

  # GET /reports/1/comments/new
  def new
    @comment = @commentable.comments.build
  end

  # GET /reports/1/comments/1/edit
  def edit
  end

  # POST /reports/1/comments
  def create
    @comment = @commentable.comments.build(comment_params)

    if @comment.save
      redirect_to [@commentable, @comment], notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /reports/1/comments/1
  def update
    if @comment.update(comment_params)
      redirect_to [@commentable, @comment], notice: 'Comment was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /reports/1/comments/1
  def destroy
    @comment.destroy
    redirect_to [@commentable, :comments], notice: 'Comment was successfully destroyed.'
  end

  private
    def set_commentable
      resource, id = request.path.split('/')[1,2]
      @commentable = resource.singularize.classify.constantize.find(id)
    end

    def set_comment
      @comment = Comment.find(params[:id])
    end

    def comment_params
      params.require(:comment).permit(:body)
    end
end

基本的には@report@commentableに変えるだけです。

ただ、1つ難しい点があります。@commentableの取得方法です。

調てみるといくつか方法があるようですが、今回は実装が簡単なPolymorphic Association in Rails 5 の方法を使います。

request情報から強引に取得します。

    def set_commentable
      resource, id = request.path.split('/')[1,2]
      @commentable = resource.singularize.classify.constantize.find(id)
    end

ちなみにこちらの方法を使ってコントローラーを分けると、より綺麗な実装になります。

以上でポリモーフィック関連のコントローラーの完成です。

あとはBookReportと同じように実装して、ビューを修正すれば課題クリアになります。

お付き合いいただき、ありがとうございました🙇