10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails6で通知機能を実装(ポリモーフィック)

Last updated at Posted at 2022-09-05

Railsで通知機能を実装した際にポリモーフィックの存在を初めて知ったのとあまり記事がなかったので参考になればと思い書いていきます

理解が間違っているかもしれません。何かあればコメントお願いします。

また、下記の記事を参考にして実装を行いました。かなり重複する内容があります。この記事はそのままではできなかったところの修正と、実装で疑問に思ったことを書いてあります。考えながら実装したい方は下記の記事から見るといいかもしれません。

今回作成するサンプル

通知機能.gif

今回通知用のページを作って実装しました
この記事ではviewは簡単な部分しか書いていません

バージョン

rails 6.1
ruby 3.1.2

前提

下記ができる状態に追加で通知機能を実装します

  • 投稿機能を実装済み(この記事では、Diaryモデル)
  • コメント機能を実装済み(この記事では、Commentモデル)
  • いいね機能を実装済み(この記事では、Favoriteモデル)
  • フォロー機能を実装済み(この記事では、Relationshipモデル)
  • gem
    • devise
    • enum_help
    • kaminari

そもそもポリモーフィックって?

簡単に言えば、
ひとつつのモデルに複数のモデル情報を保存できるってこと(多分)

要は、
複数のモデルにbodyというカラムがあった場合、ポリモーフィックモデル.bodyで取り出すことが出来るってこと(多分)

ぱっと使いどころが出てこないですね

今回の場合だと通知用のモデルにいいねやフォローをしたときの情報をそのまま入れてあげる感じ
ポリモーフィックで実装していない場合はコメントやいいねした投稿の情報をカラムに追加しないといけない。いろんな通知を受けたい場合、通知の種類の数だけカラムが増えていくことになる。それをカラムを増やさず通知の種類を増やすことができる。
今回ポリモーフィックで実装する利点はここだけです
本来?は同名のカラムや、モデルに定義した同名のメソッドなんかを、同じコードで呼び出せるところまでできるぽい。

詳しいことはRailsガイド参考にした記事、もしくはいちばん上のqiitaの記事を読んでください

通知機能では通知の種類ごとに別々の内容をだすため、内容ごとに分岐させて処理することになります。せっかくのポリモーフィックがもったいないですね。いい方法が思いつきそうなんですけど時間がかかりそうなのでこのままです。いいのがあればコメントで教えてください。

ER図

私個人のER図そのままで恥ずかしいのですがこんな感じになります。
アソシエーションの線はたぶん間違ってる気がします。もし分かれば教えてほしいです。
共有日記ER図2022.09.04.jpg
ユーザーは複数の通知を持っている
通知ひとつにモデルの情報をひとつ持っている

カラムの説明

  • 通知をもらうユーザー
    誰に対しての通知かをここに入れます

  • 通知元のモデル、通知元のID
    ここに通知が発生した時の情報を入れます

  • 通知の種類
    どういう意味の通知なのかを入れます
    (通知の種類カラムは今回、通知元のモデルとほぼ同じ内容を書くことになります
    なくてもいいのですが、同じモデルから別の種類の通知を作る際に必要になるため先に作っておきます)
    例:いいねした時と外した時のような同一モデルからの通知

  • 読んだ?
    未読かどうかの判別に使います

マイグレーション

create_activities.rb
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を行うと

schema.rb
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]

アソシエーション

model/activity.rb
  belongs_to :subject, polymorphic: true

ふたつにわかれたsubject.typeとidをひとつにまとめてsubjectであつかえます
例:activity.subject.bodyなど

model/user.rb
  has_many :activities, dependent: :destroy

ユーザーはアクティビティを複数持てます

model/favorite.rb comment.rb relationsip.rb
  has_one :activity, as: :subject, dependent: :destroy

全部各内容が同じです
activityのsubjectに関連付けます
dpendent destroyで通知元を消すと通知も消えるようにします
subjectカラムに他のモデルの情報が入るので、通知も消さないと通知を参照しに行っても情報がないのでバグってしまいます


追記:ここから下のenumの設定からコードを修正した記事を出しました。
せっかくポリモーフィックを使っているのでその利点を使った書き方をした記事になります
見比べてみるといいかもしれません


enumの設定

model/activity.rb
  enum action_type: {
    favorite:  0,
    follow:    1,
    comment:   2
  }

今回通知する内容はそれぞれのモデルで一種類づつしか入れないので簡単に書いてあります
通知を入れたい内容の分だけ好きなように増やして大丈夫です

create時に通知を入れる

通知がする内容がcreateされたときにactivityをcreateするようにします

model / favorite.rb comment.rb relationship.rb
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が動いて保存されたのが確認できます
ここで正確に保存が出来ているか確認してください
image.png
※user_idがいいねとアクティビティで同じ値なのは自分の投稿をいいねしたためです

viewに表示(コントローラーとルーティングの設定)

ここからは好きなように書いて問題ありません
未読だけ表示、同じ投稿へのいいねはまとめる、どのタイミングで見たことにするのか、いろいろカスタマイズしてください

ここではすべての通知の一覧表示と一覧表示から各通知を押したときに既読にする設定で実装しています
viewファイルのすべては書きません、レイアウトは頑張ってください

コントローラーとルートを作ります

activities_controller.rb
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ごとにリダイレクト先を分けています
    自由に設定してください


routes.rb
  resources :activities, only: [:index] do
    patch :read, on: :member
  end

resurcesに読んだ時の処理をするルートをネストで入れてます

activities/index.html.erb
<% 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をみてください

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?