8
5

More than 3 years have passed since last update.

Railsで通知機能を実装してみた

Last updated at Posted at 2020-12-01

実装する事

  • ポリモーフィック関連付けを用いた通知機能の作成
  • 通知機能一覧の表示
  • 確認した通知は表示しない

ポリモーフィック関連付けとは?

 詳細はRailsTutorialを確認してください。
 後は記事の中で簡単に説明しています。

なぜポリモーフィック関連付けで作成するのか

 通知モデルが一つのモデルに対してのみ関連づく場合はポリモーフィック関連付けを行う意味はありません。他のQiita記事を参考にされてください。
 通知モデルが複数のモデルに対して関連づく場合は是非ともポリモーフィック関連付けを行ってください。理由とポリモーフィック関連付けの説明に関しては下記に記載します。

ポリモーフィック関連付けを使用しない場合のデメリット

 通知モデルが複数のモデルに対して関連づく場合、ポリモーフィック関連付けを使わなければ以下のような設計になると思います。

  • Notificationテーブル

    • visiter_id : 通知を送ったユーザーのid
    • visited_id : 通知を送られたユーザーのid
    • comment_id : 投稿へのコメントのid
    • follow_id : フォローへのid
    • checked : ユーザーが通知を確認したかどうか default: false
  • この設計を行なった場合、DB設計的には下記のデメリットが予想されます。

    • 新しく通知モデルと関連付けしたいモデルが発生した場合、テーブルの変更(列の追加)を行う必要がある。
    • nullのデータが作成される。
  • Rails的には下記のデメリットが予想されます。

    • case文を書かなければ、親モデルにアクセスする事ができない。
    • case文の数だけ、運用性が落ちてしまう。

ポリモーフィック関連付けを使用した場合のメリット

 上記のデメリットがなくなります!!

ポリモーフィック関連付けって何?

 オブジェクト指向言語のポリモーフィズム(多態性)を用いた機能と認識しています。なので、ポリモー フィズムに関して先に簡単に説明します。

ポリモーフィズムって何?

 ポリモーフィズムは簡単に言うと、仕様をあらかじめ決定しておく事で呼び出しもとが呼び出し先のことを意識しなくても特定の機能を提供する事ができる事です。
 例えば、車を思い浮かべてください。
 車はアクセルを踏むと、前に進みますね? これは外国の車でも水素自動車でも電気自動車でも共通の仕様です。つまり下記のような仕様が決まっているんです。

  • 車が前に進むことに関しての仕様
    • 呼び出すメソッド
      • アクセルを踏む
    • 実行結果
      • 前に進む

プログラミング的に言うと下記のような感じです。

class Car
 def press_accelerator
  "前に進む"
 end
end 

class ElectricCar
 def press_accelerator
  "前に進む"
 end
end 

class HydrogenCar
 def press_accelerator
  "前に進む"
 end
end 


go_straight(Car.new)
go_straight(ElectricCar.new)
go_straight(HydrogenCar.new)

def go_straight(car)
  car.press_accelerator
  =>前に進む
end

 当然のことですよね?これがポリモーフィズムです。呼び出す側はcarという変数の中にどのようなクラスのインスタンスが入っているか意識する必要はないのです。
 では、ポリモーフィズムでない場合はどうなるでしょうか?
 アクセルを手で操作する車もありますよね?その場合、仕様と異なるためプログラミング的には下記のようになります。

class Car
 def press_accelerator
  "前に進む"
 end
end 

class HandAcceleCar
 def press_hand_accelerator
  "前に進む"
 end
end 

go_straight(Car.new)
go_straight(HandAcceleCar.new)

def go_straight(car)
  if car == Car
    car.press_accelerator
    =>前に進む
  elsif car == HandAcceleCar
    car.press_hand_accelerator
    =>前に進む
  end
end

 仕様が決まっていない場合、呼び出す側が何を呼び出せば目的を達成できるのかを意識する事が必要です。2つ3つなら良いですが、システム規模に比例するように20個、30個も呼び出すクラスと関数を意識するのは大変ですよね。そのために、ポリモーフィズム、仕様を決める事が大切なのです。

ポリモーフィック関連付けを用いた実装に関して

 ようやく本題……。
 では、Railsで通知機能を作成したいと思います。
 先にこの後登場するクラスに関してご紹介いたします。

  • 通知
    • Notification
  • コメント
    • Comment
  • フォロー  
    • follow
  • アイテム
    • Item
    • コメントとフォローの親モデル

ポリモーフィック関連付けを行わなかった場合のテーブル設計は下記の通りです。

  • Notificationテーブル
    • visiter_id : 通知を送ったユーザーのid
    • visited_id : 通知を送られたユーザーのid
    • comment_id : 投稿へのコメントのid
    • follow_id : フォローへのid
    • checked : ユーザーが通知を確認したかどうか default: false

マイグレーションファイルに関して

 マイグレーションファイルの中身は下記の形です。

app/db/migrate/?????????_create_notification.rb
  def change
    create_table :notifications do |t|
      # 通知をされる人のidを保存しています。
      t.references :notify, foreign_key: { to_table: :users }
      # 通知する人のidを保存しています。
      t.references :notified, foreign_key: { to_table: :users }
      # DBではtypeとidに分かれます。がそれを意識する必要はありません。
      t.references :event, polymorphic: true
      # 新規作成なのか、更新なのかを保持しています。
      t.integer :action, defalut: 0
      # 確認したかどうかを保持しています。
      t.boolean :checked, default: false, null: false

      t.timestamps
    end
  end

 polymorphic: trueをつける事で、Railsにポリモーフィック関連付けを明示しています。
 eventは私が考えた名前なので、お好きなように変更してください。
 命名に関してはモデルの関連付けを読んだ後に考えるとそれらしくなるかもしれません。

 ちなみに上記のマイグレーションファイルで出来るスキーマは下記の通りです。

db/schema.rb
  create_table "notifications", force: :cascade do |t|
    t.bigint "notify_id"
    t.bigint "notified_id"
    t.string "event_type"
    t.bigint "event_id"
    t.integer "action"
    t.boolean "checked", default: false, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["event_type", "event_id"], name: "index_notifications_on_event_type_and_event_id"
    t.index ["notified_id"], name: "index_notifications_on_notified_id"
    t.index ["notify_id"], name: "index_notifications_on_notify_id"
  end

モデルの関連付けに関して

通知クラスに対して関連付けを行います。

まず通知クラスに対しての関連付けを記載いたします。

app/models/notification.rb
class Notification < ApplicationRecord
  #こいつが関連付け
  belongs_to :event, polymorphic: true

  belongs_to :notify, class_name: 'User', foreign_key: 'notify_id'
  belongs_to :notified, class_name: 'User', foreign_key: 'notified_id'

  enum action: { create_item: 0, edit_item: 1 }
end

 belongs_to :event, polymorphic: true
 この一文がポリモーフィック関連付けを行なっています。超重要です。eventモデルの事を書き忘れているわけではありません。
 このeventを通じて、他のモデルにアクセスする事ができます。
 が! ここで詳細を説明するのは少し情報が不足しているので後に回します。
 ここでは、eventはマイグレーションファイルを作成したときにpolymorphic: trueで指定した列名と同じにする事と覚えておいてください。

コメントクラスとフォロークラスに対して関連付けを行います。

app/models/comment.rb
class Comment < ApplicationRecord
  has_many :notifications, as: :event
end
app/models/follow.rb
class Follow < ApplicationRecord
  has_many :notifications, as: :event
end

ルーティングを設定します。

config/routes.rb
Rails.application.routes.draw do
    resources :notifications, only: [:index, :update]
end

通知コントローラーを作成します。

app/contorollers/notifications_controller.rb
class NotificationsController < ApplicationController
  def index
    @notifications = Notification.where(notify: current_user.id).where(checked: false).order(created_at: :desc).limit(10)
    respond_to do |format|
      format.js { render :index }
    end
  end

  def update
  end

コメントコントローラーとフォローコントローラーを作成します。

app/contorollers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @item = Item.find(params[:item_id])
    comment = @item.comments.build(comment_params)
    comment.user_id = current_user.id
    comment.notifications.build({notify: @item.user, notified: current_user, action: 0})
    comment.save
    @comments = Comment.where(item_id: @item.id)
    respond_to do |format|
      format.js { render :index }
    end
end
app/contorollers/follows_controller.rb
class FollowsController < ApplicationController
  def create
    @user = User.find(params[:item_id])
    follow = @user.follows.build(follow_params)
    follow.user_id = @user.id
    follow.notifications.build({notify: @item.user, notified: current_user, action: 0})
    follow.save
    redirect_to user_path(@user)
end

フロント部分を作成します。

app/views/notifications/index.js.erb
$(".notifications_list").html("<%= j(render 'notifications/index', { notifications: @notifications }) %>")
app/views/notifications/index.html.erb
<ul class="notifications_list">
  <% if notifications.blank? %>
    <li><a href="#">通知はありません</a></li>
  <% else %>
    <% notifications.each do |notification| %>
      <% if notification.create_item? %>
        <li><%= link_to notification.event.notification_create_message, notification_path(id: notification.id), method: :put  %></li>
      <% else %>
        <li><%= link_to notification.event.notification_update_message(notification.notified.name), notification_path(id: notification.id), method: :put  %></li>
      <% end %>
    <% end %>
  <% end %>
</ul>

ふぅ。
お疲れ様でした。怒涛のコーディングでしたね。
ここで、一旦心を落ち着けて今記載したコードをよく見てください。

<li><%= link_to notification.event.notification_create_message, notification_path(id: notification.id), method: :put %></li>

notification.event......通知クラスの関連付けで記載したeventがようやく登場しました。
では一旦、この文章を分解していきましょう。

  • link_to
    • を作るためのメソッドですね
  • notification.event.notification_create_message
    • ポリモーフィック関連付けの肝
  • notification_path(id: notification.id), method: :put
    • notifications_controllerのupdateに行くように設定されています。

理解できましたか?
そう、notification.event.notification_create_messageを理解するにはまだ情報が足りません! では、理解するための情報を追加しましょう。
コメントクラスとフォロークラスに対してメソッドを追加します。

app/models/comment.rb
class Comment < ApplicationRecord
  has_many :notifications, as: :event

  def notification_create_message
    "新しくコメントがされました"
  end
  def notification_edit_message(edit_user)
    "#{edit_user}によりコメントが編集されました"
  end
end
app/models/follow.rb
class Follow < ApplicationRecord
  has_many :notifications, as: :event

  def notification_create_message
    "新しくフォローがされました"
  end
  def notification_edit_message(edit_user)
    "#{edit_user}によりフォローが変更されました"
  end
end

さて、これで先ほどのをもう一度確認します。
notification.event.notification_create_message
両方のクラスにnotification_create_messageが定義されていますね。
これを実行した場合はイメージ的には以下のような動きになっています。

  1. notification.eventにはCommentモデルかFollowモデルのインスタンスが入っている。
  2. notification.eventに入っているのがCommentかFollowかは解らないが、両方のクラスにnotification_create_messageは定義されている。
  3. notification_create_messageを実行しよう。

です。
この2番の効力はすごくないですか?
本来なら、notification.commentやnotification.followと指定しなければいけないんですよ。
つまり、以下のコードが必要になりますよね?

# ポリモーフィック関連付けを用いて作成した場合には不要なコードです。
if notification.event_type == "Comment"
        <li><%= link_to notification.comment.notification_create_message, notification_path(id: notification.id), method: :put  %></li>
else
        <li><%= link_to notification.follow.notification_create_message, notification_path(id: notification.id), method: :put  %></li>
end

これが、必要ないので、すごく見易くかけていますよね?それに、新しく通気機能を入れたい場合は対象のモデルに処理を追加するだけで済むので変更が少なくてすみますよね?

通知コントローラーの更新処理を作成します。

app/contorollers/notifications_controller.rb
class NotificationsController < ApplicationController
  def index
    @notifications = Notification.where(notify: current_user.id).where(checked: false).order(created_at: :desc).limit(10)
    respond_to do |format|
      format.js { render :index }
    end
  end

  def update
    notification = Notification.find_by(id: params[:id])
    notification.update(checked: true)
    params = notification.event.notification_params_hash
    redirect_to polymorphic_path([params[:path_model]])
  end

polymorphic_pathはモデルのインスタンスから適切なpathを自動生成してくれます。
今回はshowを指定しています。
また、新しくメソッドを定義しているので、それを作成しましょう。

app/models/comment.rb
class Comment < ApplicationRecord
  has_many :notifications, as: :event

  def notification_create_message
    "新しくコメントがされました"
  end
  def notification_edit_message(edit_user)
    "#{edit_user}によりコメントが編集されました"
  end
  def notification_params_hash
    hash = {}
    hash[:path_model] = self.item
    hash
  end
end
app/models/follow.rb
class Follow < ApplicationRecord
  has_many :notifications, as: :event

  def notification_create_message
    "新しくフォローがされました"
  end
  def notification_edit_message(edit_user)
    "#{edit_user}によりフォローが変更されました"
  end
  def notification_params_hash
    hash = {}
    hash[:path_model] = self.user
    hash
  end
end

これで終了です!
お疲れ様でした!!

反省

書いている中で、口調が統一されていなかった。
見辛い可能性もあるので、今後要改善ですね……。

8
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
8
5