Railsで通知機能を実装した際にポリモーフィックの存在を初めて知ったのとあまり記事がなかったので参考になればと思い書いていきます
理解が間違っているかもしれません。何かあればコメントお願いします。
また、下記の記事を参考にして実装を行いました。かなり重複する内容があります。この記事はそのままではできなかったところの修正と、実装で疑問に思ったことを書いてあります。考えながら実装したい方は下記の記事から見るといいかもしれません。
今回作成するサンプル
今回通知用のページを作って実装しました
この記事ではviewは簡単な部分しか書いていません
バージョン
rails 6.1
ruby 3.1.2
前提
下記ができる状態に追加で通知機能を実装します
- 投稿機能を実装済み(この記事では、Diaryモデル)
- コメント機能を実装済み(この記事では、Commentモデル)
- いいね機能を実装済み(この記事では、Favoriteモデル)
- フォロー機能を実装済み(この記事では、Relationshipモデル)
- gem
- devise
- enum_help
- kaminari
そもそもポリモーフィックって?
簡単に言えば、
ひとつつのモデルに複数のモデル情報を保存できるってこと(多分)
要は、
複数のモデルにbodyというカラムがあった場合、ポリモーフィックモデル.bodyで取り出すことが出来るってこと(多分)
ぱっと使いどころが出てこないですね
今回の場合だと通知用のモデルにいいねやフォローをしたときの情報をそのまま入れてあげる感じ
ポリモーフィックで実装していない場合はコメントやいいねした投稿の情報をカラムに追加しないといけない。いろんな通知を受けたい場合、通知の種類の数だけカラムが増えていくことになる。それをカラムを増やさず通知の種類を増やすことができる。
今回ポリモーフィックで実装する利点はここだけです
本来?は同名のカラムや、モデルに定義した同名のメソッドなんかを、同じコードで呼び出せるところまでできるぽい。
詳しいことはRailsガイド、参考にした記事、もしくはいちばん上のqiitaの記事を読んでください
通知機能では通知の種類ごとに別々の内容をだすため、内容ごとに分岐させて処理することになります。せっかくのポリモーフィックがもったいないですね。いい方法が思いつきそうなんですけど時間がかかりそうなのでこのままです。いいのがあればコメントで教えてください。
ER図
私個人のER図そのままで恥ずかしいのですがこんな感じになります。
アソシエーションの線はたぶん間違ってる気がします。もし分かれば教えてほしいです。
ユーザーは複数の通知を持っている
通知ひとつにモデルの情報をひとつ持っている
カラムの説明
-
通知をもらうユーザー
誰に対しての通知かをここに入れます -
通知元のモデル、通知元のID
ここに通知が発生した時の情報を入れます -
通知の種類
どういう意味の通知なのかを入れます
(通知の種類カラムは今回、通知元のモデルとほぼ同じ内容を書くことになります
なくてもいいのですが、同じモデルから別の種類の通知を作る際に必要になるため先に作っておきます)
例:いいねした時と外した時のような同一モデルからの通知 -
読んだ?
未読かどうかの判別に使います
マイグレーション
class CreateActivities < ActiveRecord::Migration[6.1]
def change
create_table :activities do |t|
t.references :user, foreign_key: true #通知をもらうユーザー
t.references :subject, polymorphic: true #通知元のモデルの情報とID
t.integer :action_type, null: false #通知の種類
t.boolean :read, null: false, default: false #読んだ?defaultで読んでないを入れます
t.timestamps
end
end
end
db:migrataを行うと
create_table "activities", force: :cascade do |t|
t.integer "user_id"
t.string "subject_type"
t.integer "subject_id"
t.integer "action_type", null: false
t.boolean "read", default: false, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["subject_type", "subject_id"], name: "index_activities_on_subject"
t.index ["user_id"], name: "index_activities_on_user_id"
end
subjectをreference型で書くことでpolymorphic:trueにできるようになります。
ひとつの文でモデルタイプのカラムとIDのカラムが作られます
ばらばらで書くこともできます
t.references :subject, polymorphic: true
#これをばらすと↓
create_table :activities do |t|
t.string :subject_type
t.integer :subject_id
#省略
end
add_index :activities, [:subject_type, :subject_id]
アソシエーション
belongs_to :subject, polymorphic: true
ふたつにわかれたsubject.typeとidをひとつにまとめてsubjectであつかえます
例:activity.subject.bodyなど
has_many :activities, dependent: :destroy
ユーザーはアクティビティを複数持てます
has_one :activity, as: :subject, dependent: :destroy
全部各内容が同じです
activityのsubjectに関連付けます
dpendent destroyで通知元を消すと通知も消えるようにします
subjectカラムに他のモデルの情報が入るので、通知も消さないと通知を参照しに行っても情報がないのでバグってしまいます
追記:ここから下のenumの設定からコードを修正した記事を出しました。
せっかくポリモーフィックを使っているのでその利点を使った書き方をした記事になります
見比べてみるといいかもしれません
enumの設定
enum action_type: {
favorite: 0,
follow: 1,
comment: 2
}
今回通知する内容はそれぞれのモデルで一種類づつしか入れないので簡単に書いてあります
通知を入れたい内容の分だけ好きなように増やして大丈夫です
create時に通知を入れる
通知がする内容がcreateされたときにactivityをcreateするようにします
after_create_commit :create_activities
private
def create_activities
Activity.create!(subject: self, user_id: ユーザーのID, action_type: Activity.action_types[:enumで設定した内容])
end
各モデルにafter_create_commitを入れます。
これはDB保存が完了した時をトリガーに動きます。
詳しくはRailsガイドをみてください
今回入れたい通知は保存したタイミングで大丈夫ですが内容によって好きなタイミングでcreate_activityが動くように書いてください
create_activitiesの説明
-
subject: self
これでDB保存ができた内容がはいります(typeとid) -
user_id: ユーザーのID
DB保存した内容からuser_idを取り出して書いてください
例:いいねのとき
user_id: diary.user.id(いいねに紐づいてる投稿に紐づいてるユーザーのID) -
action_type: Acticity.action_types[:enumで設定した内容]
右で書いた文はenumで設定した内容から値を出力しています
例:いいねのとき
action_type: Activity.action_types[:favorite]これでaction_typeに0が入ります
ターミナルで確認してみます
いいねが保存された後にcreate_activitiesが動いて保存されたのが確認できます
ここで正確に保存が出来ているか確認してください
※user_idがいいねとアクティビティで同じ値なのは自分の投稿をいいねしたためです
viewに表示(コントローラーとルーティングの設定)
ここからは好きなように書いて問題ありません
未読だけ表示、同じ投稿へのいいねはまとめる、どのタイミングで見たことにするのか、いろいろカスタマイズしてください
ここではすべての通知の一覧表示と一覧表示から各通知を押したときに既読にする設定で実装しています
viewファイルのすべては書きません、レイアウトは頑張ってください
コントローラーとルートを作ります
class Public::ActivitiesController < ApplicationController
before_action :authenticate_user
def index
@activities = current_user.activities.order(created_at: :desc).page(params[:page]).per(20)
end
def read
activity = current_user.activities.find(params[:id])
unless activity.read?
activity.update(read: true)
end
redirect_to transition_path(activity)
end
def transition_path(activity)#アクションタイプごとにリダイレクト先を指定
case activity.action_type
when 'favorite'
user_diary_path(activity.subject.diary.user.name_id,activity.subject.diary.id)
when 'comment'
user_diary_path(activity.subject.diary.user.name_id,activity.subject.diary.id)
when 'follow'
user_path(activity.subject.follower.name_id)
end
end
end
コントローラーの説明
-
before_action :authenticate_user
deviseのメソッドでユーザー以外をはじきます -
index
表示したい内容をkaminariを使って20件ずつ作成日時の新しい順でいれます -
read
通知一覧から各通知をクリックした際にすべてここを通して処理します
通知を読んだらtrueにして各通知元へリダイレクトさせます -
transition_path(ativity)
action_typeごとにリダイレクト先を分けています
自由に設定してください
resources :activities, only: [:index] do
patch :read, on: :member
end
resurcesに読んだ時の処理をするルートをネストで入れてます
<% if @activities.present? %>
<% @activities.each do |activity| %>
<%= link_to read_user_activity_path(current_user.name_id,activity), method: :patch do %>
<%= render "#{activity.action_type}", activity: activity %>
<% end %>
<%= paginate @activities %>
<% end %>
<% else %>
<p>通知はありません</p>
<% end %>
今回参考にした記事で書いてあったのですが、renderのなかにaction_typeをかくことでifを使わずに表示させる内容を変えています。
enumで設定した名前ごとに部分テンプレートを作成します
実装は以上です
まとめ
今回は通知機能をポリモーフィックを使って実装していきました
ポリモーフィック便利そうなので色々調べてみてください
通知機能をいれるために覚えておくこと
- 通知用でモデルが必要
- 任意のタイミングで通知モデルをcreate
ポリモーフィックで覚えておくこと
- モデル情報ごと保存することが出来る
最後に
通知機能をポリモーフィックで作りましたがポリモーフィックで得られるメリットがそこまで大きくないのかなと感じた(カラムの追加をしなくてもいいのは楽だけど)
ポリモーフィック自体はしっかり使えると便利だと思うので使いどころを考えていきたい
初めての記事なのであたたかい目で見てくれると助かります
もしサンプル動画のコードが見たい場合はgithubをみてください