ポリモーフィックについて
今回はポリモーフィック関連について書いていこうかと思います。
ポリモーフィックとは複数の異なる型やオブジェクトに対して共通のインターフェースを備えることです。 ポリモーフィック関連付けを使用すると、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現することができます。
例えば、映画レビューサイトを作る場合、Movieモデル、Actorモデル、Commentモデルを用意して作品や出演者のレビューコメントが投稿できるようにコメントモデルには中間テーブルを作成し、それぞれ関連付けを行う必要があるかと思います。
この場合、モデルが増える(監督についてもモデルを作ってレビューコメント機能をつけたい等)たびに、中間テーブルを用意し関連付けをする必要があり非常に面倒です。
そこで共通のインターフェースを用意して、それにCommentを関連付けて一つのモデル(comment)だけで処理をしようというのが、ポリモーフィック関連です!
modelの作成
ポリモーフィック関連付けの実装自体はいたってシンプルです。
まずはターミナルでcommentモデルを作成します。
rails g model comment content:text commentable:references{polymorphic}
ポイントはcommentable:references{polymorphic}
マイグレーションファイルは下記のようになります。
class CreateComments < ActiveRecord::Migration[5.2]
def change
create_table :comments do |t|
t.text :content
t.references :commentable, polymorphic: true
t.timestamps
end
end
end
カラムはcontent以外に自動でcommentable_typeとcommentable_idが生成されます。
commentable_typeにはMovieもしくはActorのモデル名が入ります。
commentabel_idはMovieもしくはActorのidが入ります。
commentable_typeではどのモデルに対してのcommentなのか、commentable_idではそのモデルのどのidに対してのcommentなのかを判別するカラムとなります。
ポリモーフィック関連付け(アソシエーション)
db:migrateでモデルが作成できれば、それぞれのモデルに関連付けを行なっていきます。
has_many :comments, as: :commentable
has_many :comments, as: :commentlable
belongs_to :commentable, polymorphic: true
以上で関連付けは終了です。
あとcontrollerとroutes、view側はポイントを絞って解説します。
controllerの処理
まずはcontroller側ですが、commentsはネストされている状態なので、commentの処理をする際にはMovieかActorのid(どちらに紐づいているコメントかを判別)が必須になります。単独のネストのid取得だとcomments_controllerでMovie.find(params[movie_id])等で設定が可能ですが、ポリモーフィックの場合はMovieかActorかが不明なので、両方に対応した書き方が必要です。
まずcontrollersにmovieとactorのフォルダを作成し、その直下にそれぞれcomments_controller.rbを作成し、CommentsControllerを継承しているMovie::CommentsControllerとActor::CommentsControllerを用意して、private以下にそれぞれのidを取得する実装をします。
class Movie::CommentsController < CommentsController
before_action :set_commentable, only: %i[create]
private
def set_commentable
@commentable = Movie.find(params[:movie_id])
end
end
class Actor::CommentsController < CommentsController
before_action :set_commentable, only: %i[create]
private
def set_commentable
@commentable = Actor.find(params[:actor_id])
end
end
before_actionでset_commentableをすることによって@commentable(movie_idかactor_id)が動的に取得できるようになります。
例えば、以下のcommentのcreate時に状況によってそれぞれ(movieもしくはactor)のモデルに紐づくcommentインスタンスをbuild生成することができます。
class CommentsController < ApplicationController
def create
@comment = @commentable.comments.build(comments_params)
if @comment.save
.
.
また独自の処理をさせたい場合は、オーバーライドすることも可能です。
class Actor::CommentsController < CommentsController
before_action :set_commentable, only: %i[create]
def create
super
flash[:notice] = 'actorのコメントを送信しました!'
end
private
def set_commentable
@commentable = Actor.find(params[:actor_id])
end
end
routesについて(module)
上記のcontrollerファイル構成変更に伴い、routesのmoduleの記述が必要です。
これでbefore_action :set_commentableが反映されます。
resources :movies do
resources :comments, only: [:create], module: :movies
end
resources :actors do
resources :comments, only: %i[create], module: :actors
end
viewについて
最後に上記で説明した部分に関わってくるview側の記述を書いておきます。
<%= @movie.name %>
<%= render 'comments/comments', commentable: @movie, comment: @comments %>
<%= @actor.name %>
<%= render 'comments/comments', commentable: @actor, comment: @comments %>
<%= form_with model: [commentable, Comment.new] do |f| %>
<%= f.text_area :content %>
<div class="actions">
<%= f.submit "コメントをする", class: 'btn btn-warning px-5' %>
</div>
<% end %>
form_withのcommentableには@movieか@acotrが入っているため、ここでmovie側のcommentかactor側のcommentかを振り分けています。
以上、終わりです。