imotanの気になる木

21歳、大学4年(未就活)の意識高い系の大学生の日記的ブロクです。

回答とコメントが時系列順に並んだページを作る

はじめに

今回は、かなり悩まされた複数のテーブルの情報を同じ配列に入れて一つのタイムライン上で表示する方法を解説します。

回答とコメントが一緒に入ったインスタンス配列を作る

f:id:imo-tyan:20181015215836p:plain

gituhubのプルリク画面をイメージして作っています。作成されたのが古い順に並べています。

 

ポリモーフィック関連

以上のようなイメージを実現するための手段としてポリモーフィック関連という方法を用います。ポリモーフィックとはある一つのモデルが複数のモデルに属しているのを一つのアソシエーションの定義だけで表現します。通常のアソシエーションで表現した際に生じる以下の課題を解決しています。以下はimageがcompanyとuserに属する関係の場合の具体例です。

  1. 新たにImageが従属するモデルを作った際、Imageのbelongs_toを増やす必要がある(=カラムを追加しないといけない)
  2. Imageモデルのインスタンスから直接、従属しているモデル(CompanyなのかUseを判別する事ができない。)

コード例

class Image < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
end

class Company < ActiveRecord::Base
  has_many :images, as: :imageable
end

class User < ActiveRecord::Base
  has_many :images, as: :imageable
end

テーブルのカラム例

f:id:imo-tyan:20181015221441p:plain

imageable_typeにはCompanyやUserなどの所属するクラス名がimageable_idにはリレーションしているインスタンスのidが格納されている

 

#例1:Companyモデルに帰属する複数の画像を取得する
company = Company.find(1)
company.images
#=> [<Image id:1 ..>,<Image id:2 ..>]

#例2:Userモデルに帰属する複数の画像を取得する
user = User.find(1)
user.images
#=> [<Image id:3 ..>,<Image id:4 ..>]

例1、例2はhas_many定義により所属する画像を取得しています。通常のアソシエーションとの違いとしては、親であるcompany, もしくはuserのidを保持するカラム(imageable_id,imageable_type)はあるが、それがcompany_idやuser_idといった名前ではない、という点です。

#例3:Imageモデルが帰属するインスタンスを取得する(帰属先が企業の場合)
image = Image.find(1)
image.imageable
#=> <Company id:1 ..>

#例4:Imageモデルが帰属するインスタンスを取得する(帰属先がユーザーの場合)
image = Image.find(2)
image.imageable
#=> <User id:1 ..>

例3、例4は、Imageが帰属するインスタンスを取得しています。image.imageableと記述するだけで関連先のインスタンスを取得することができます。ここで重要なのは、Imageクラスのインスタンスがどのモデルに帰属しているかが分からなくも.imageableと記述するだけで関連先のインスタンスを取得できるという点です。

#例5: Companyモデルに帰属するImageクラスのインスタンスを作成する
company = Company.create
Image.create(imageable_id: company.id,imageable_type: company.class.to_s)
#=>  INSERT INTO `Images` (`imageable,_id`, `imageable,_type`, `created_at`, `updated_at`) VALUES (1, 'Company', '2015-0x-0x..', '2015-0x-0x..')

#例6: Companyモデルに帰属するImageクラスのインスタンスを作成する
company = Company.create
company.images.create
#=>  INSERT INTO `Images` (`imageable,_id`, `imageable,_type`, `created_at`, `updated_at`) VALUES (1, 'Company', '2015-0x-0x..', '2015-0x-0x..')

例5と例6は同じ処理を行っており、どちらもimageクラスのインスタンスを生成しています。

回答とコメント用のモデルを作る

では、ポリモーフィック関連を利用して、回答とコメント用のモデルを生成していきます、ここのモデル名はreviewモデルにします。

app/model/answer_commnet_content.rb

class Review < ActiveRecord::Migration
  def change
    create_table :reviews do |t|
      t.integer :content_id
      t.string  :content_type
      t.integer :question_id
      t.timestamps
    end
  end
end

ポリモーフィック関連の定義

app/models/AnswerCommentContent

class Review < ActiveRecord::Base
  belongs_to :content, polymorphic: true
end

app/models/Comment

class Comment < ActiveRecord::Base

  has_one :review, as: :content, dependent: :destroy

app/models/Answer

class Answer < ActiveRecord::Base

  has_one :review, as: :content, dependent: :destroy

質問、回答の投稿時にAnswerCpmmentContentsも同時に追加する

ポリモーフィック関連を利用するためには、質問及び回答が投稿された時にReviewを同時に追加する必要があります。

class QuestionsController < ApplicationController
  def create
    question = Question.create(create_params)
    question.review = Review.create(group_id: question.group_id, updated_at: question.updated_at)
    redirect_to :root and return
  end

コールバックを利用して部分的に分けるのと良いです。

ビューにインスタンスの配列を渡す

質問の詳細ページで、質問に対する回答とコメントを取得するためには、該当の質問のreviewを取得する必要がある。

class QuestionController < ApplicationController
  def show
    review = current_user.question.reviews.includes(:content)
    @review = reviews.map(&:content) //reviewsからcontentのみ取得する
  end
end

コントローラから渡したインスタンス配列でタイムラインを表示する

ビューはインスタンスの種類によってビューを出し分けたいと思います。

<%= render @reviews %>

renderメソッドにインスタンス配列を渡した時は、インスタンスに応じて部分テンプレートを自動で呼び出し分けてくれます。
つまり、Answerのインスタンスだった場合は、app/views/answers/_answer.html.erbを呼び、Commentのインスタンスだった場合はapp/views/questions/_comment.html.erbを呼び出します。 

 

非常に長い解説になりましたが、非常に便利な機能ですね。絶対使いこなしたい機能であることは間違いない。。。