ポリモーフィック関連のコントローラー
まだまだ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
参考: nested_scaffold/controller.rb at master · amatsuda/nested_scaffold · GitHub
ポリモーフィックなルーティング
上記のコードでは、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
をいい感じに解釈して、@commentable
がreportオブジェクト
の場合は/reports/1/comments/1
というURLを作り、@commentable
がbookオブジェクト
の場合は/books/1/comments/1
というURLを作ってくれます。
このコード1つで2つのURLに対応してくれます。
ここでreport_comment_path(@report, @comment)
と書いてしまうと、後に分岐処理を書くことになり、ポリモーフィック関連の強みを活かせません。
そのため[@report, @comment]
という書き方を利用しています。
ちなみに[@report, @comment]
のような書き方は、redirect_to
だけでなくlink_to
やform_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
参考: Active Record の関連付け - Railsガイド
これでも良いのですが、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という機能を使っています。
参考: [Rails] ActiveSupport::Concern の存在理由 - Qiita
これでreport - comments
だった関連がcommentable - comments
となり、抽象化できました。
Bookでinclude Commentable
することで、book
とreport
を同じ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
ちなみにこちらの方法を使ってコントローラーを分けると、より綺麗な実装になります。
以上でポリモーフィック関連のコントローラーの完成です。
あとはBook
をReport
と同じように実装して、ビューを修正すれば課題クリアになります。
お付き合いいただき、ありがとうございました🙇