🔔

【Rails】モーダルで開く通知機能①(フォロー・いいね・コメント・コメントいいね) - モデル・コントローラ作成 -

2023/09/29に公開

通知機能はいれてみたい!と思いポートフォリオに通知機能を実装しました。
どなたかの参考になれば嬉しいです。

完成イメージ

  • 通知があると通知アイコンに件数が表示される
  • 通知アイコンをクリックするとモーダルウィンドウが開く
  • モーダルウィンドウを開いたら通知アイコンの件数が消える

実装する通知機能

  1. フォロー通知
  2. 投稿へのいいね通知
  3. 投稿へのコメント通知
  4. コメントへのいいね通知

よって以下の実装が終わっている状態です。

  • フォロー機能(Relationshipモデル)
  • 投稿機能(Postモデル)
  • いいね機能(Post_favoriteモデル)
  • コメント機能(Commentモデル)
  • コメントへのいいね機能(Comment_favoriteモデル)

通知機能として、全てを実装させる必要はないので、自身のサイトにカスタマイズしていただければと思います。

流れ

  1. モデル作成と関連付け
  2. 各メソッドの作成
  3. コントローラ呼び出し
  4. 通知ページのルーティング設定
  5. 通知ページのコントローラ設定
  6. ビュー作成

この記事では2. 各メソッドの作成まで解説します。

ER図 / テーブル


通知機能を入れたER図は上記のようにしました。

解説:

  • visitor_id:
    通知を送ったユーザーのID。

  • visited_id:
    通知を送られたユーザーのID。

  • 各テーブルid
    通知をつくりたいテーブルを紐づけます。

  • action:
    通知の種類を格納するカラムです。例えば、「follow」はフォロー通知、「favorite_post」は記事いいね通知など。

  • is_checked:
    通知を受信したユーザーが通知を確認したかどうかを示すフラグです。

モデル作成

ターミナルでモデルを作成します。

ターミナル
rails g model Notification

マイグレーションファイル編集

マイグレーションファイルを以下のように編集します。

xxxxxx_create_notifications.rb
class CreateNotifications < ActiveRecord::Migration[6.1]
  def change
    create_table :notifications do |t|
      t.integer :visitor_id, null: false
      t.integer :visited_id, null: false
      t.integer :post_id
      t.integer :comment_id
      t.integer :post_favorite_id
      t.integer :comment_favorite_id
      t.integer :relationship_id
      t.string :action, null: false, default: ''
      t.boolean :is_checked, null: false, default: false

      t.timestamps
    end
  end
end

マイグレーションします。

ターミナル
rails db:migrate

actionカラムの解説

t.string :action, null: false, default: ''
  • null: false
    通知の種類は必ず設定されるべきなのでNILL値は許容しません。

  • default: ''
    このカラムに値が設定されない場合のデフォルト値を指定しています。ここでは空の文字列''をデフォルト値として設定しています。通知の種類が指定されない場合、自動的に空の文字列が格納されます。

defalut値の必要性

具体例をもとにデフォルト値の説明をします。

例えば、以下の通知があるとします。

  • 通知のID
  • 通知を送ったユーザーのID(通知の送信者)
  • 通知を受け取るユーザーのID(通知の受信者)
  • 通知の種類(フォロー通知など)

通知の種類 (action) に関して、以下の状況を考えてみます:

  1. 通知がフォロー通知の場合:通知の種類は "follow" です。

  2. 通知がいいね通知の場合:通知の種類は "favorite_post" または "favorite_comment" です。

  3. 通知がコメント通知の場合:通知の種類は "comment" です。

ここでデフォルト値の必要性が明確になります。

状況 1:通知がフォロー通知の場合

フォロー通知の場合、通知の種類は "follow" と明確です。したがって、データベースに通知を保存する際には "follow" という値を action カラムに設定します。

状況 2:通知がいいね通知の場合

いいね通知の場合、通知の種類は "favorite_post" または "favorite_comment" です。ただし、何らかの理由(バグやデータベースの手動操作等)で通知の種類が指定されない場合、どの値を action カラムに格納すればよいでしょうか?ここでデフォルト値が役立ちます。デフォルト値を "unknown" や空の文字列 '' などに設定しておけば、通知の種類が指定されない場合にもデータベースに不正確な値を保存することを防ぐことができます。

状況 3:通知がコメント通知の場合

同様に、コメント通知の場合も通知の種類は "comment" ですが、通知の種類が指定されない場合に備えてデフォルト値を設定しておくことで、データベースの整合性を保ちます。

デフォルト値は、データベースのカラムに値が設定されない場合に設定される「バックアップ」のような値です。デフォルト値を設定することで、アプリケーションが予期しないデータの状態になることを防ぎ、データベースの信頼性を高めます。

is_checkedカラムの解説

t.boolean :is_checked, null: false, default: false
  • t.boolean:
    trueかfalseのどちらかを格納するためのブール型にします。
  • null: false
    trueかfalseのどちらかが入るのでnullにできないようにします。
  • default: false
    既読になったらtrueにしたいので、デフォルトはfalseです。

モデルの関連付け

各モデルと通知モデルを紐づけていきます。

Userモデル

app/models/user.rb
has_many :active_notifications, class_name: "Notification", foreign_key: "visitor_id", dependent: :destroy
has_many :passive_notifications, class_name: "Notification", foreign_key: "visited_id", dependent: :destroy
  • active_notificatons:
    自分からの通知

  • passive_notifications:
    相手からの通知

  • class_name: "Notification":
    Notificationモデルと関連付け

  • foreign_key:
    外部キーを設定することで、Userモデルのidカラムが、Notificationモデルのvisitor_idvisited_idと関連付けられる。

  • dependent: :destroy:
    ユーザーが削除されると、そのユーザーに関連する通知も削除

各モデル(Post, Comment, Comment_favorite_ Post_favorite)

app/models/xxxxx.rb
has_many :notifications, dependent: :destroy

Notificationモデル

app/models/notification.rb
class Notification < ApplicationRecord
  
  default_scope -> { order(created_at: "DESC") }

  belongs_to :post, optional: true
  belongs_to :comment, optional: true
  belongs_to :post_favorite, optional: true
  belongs_to :comment_favorite, optional: true
  belongs_to :relationship, optional: true

  belongs_to :visitor, class_name: "User", foreign_key: "visitor_id", optional: true
  belongs_to :visited, class_name: "User", foreign_key: "visited_id", optional: true
end
  • default_scope -> { order(created_at: "DESC") }:
    レコードのデフォルトの並び順を降順(新しいものから古いものへ)にする。

  • optional: true:
    関連付けられたデータがなくても大丈夫、という設定。

optional: true

optional: true の設定は、関連付けられたオブジェクトが存在しない場合に、データベースへの保存を許可するためのオプションです。このオプションを設定することで、関連がない場合でも通知レコードを作成できるようになります。

例えば、

  1. belongs_to :post, optional: true の場合:
    ユーザーが特定の投稿に関連する通知を受けたとします。
    その投稿が後で削除された場合、通知は削除されずに残ります。 optional: true が設定されているため、通知が関連する投稿が存在しなくても通知を保存することができます。

  2. belongs_to :relationship, optional: true の場合:
    1同様、optional: true を設定することで、フォロー関係が削除されても通知が残ります。

モデルへ通知メソッド作成

フォローの通知

app/model/user.rb
  # フォロー通知を作成するメソッド
  def create_notification_follow!(current_user)
    # すでにフォロー通知が存在するか検索

    existing_notification = Notification.find_by(visitor_id: current_user.id, visited_id: self.id, action: 'follow')

    # フォロー通知が存在しない場合のみ、通知レコードを作成
    if existing_notification.blank?
      notification = current_user.active_notifications.build(
        visited_id: self.id,
        action: 'follow'
      )
      notification.save if notification.valid?
    end
  end
解説
1
def create_notification_follow!(current_user)
  • ここでの!は「バンメソッド」とも呼ばれ、使うのに注意が必要だよ!という意味です。つけてもつけなくても問題はありません。
  • (current_user) はメソッドの引数で、通知を送るユーザーを表します。
2
existing_notification = Notification.find_by(visitor_id: current_user.id, visited_id: self.id, action: 'follow')
  • existing_notification は変数で、通知がすでに存在する場合にその通知を格納します。

  • Notification.find_by(...) は、データベースから通知を探し出すためのメソッドです。

  • visitor_id: current_user.id, visited_id: self.id, action: 'follow':これは検索条件です。この部分では、3つの条件を指定しています。

    1. visitor_idcurrent_user.id と一致する通知を検索します。ここで current_user は、このメソッドに渡された現在のユーザーを表します。
    2. visited_idself.id と一致する通知を検索します。self はこのメソッドが呼び出されたオブジェクトを指し、通常はユーザーを表します。
    3. action'follow' と一致する通知を検索します。この条件では、通知の種類がフォロー通知であるかどうかをチェックしています。

    このコードを通じて、特定のユーザー(current_user)から別のユーザー(self、フォローされたユーザー)に対するフォロー通知がすでに存在するかどうかを調べています。もし既存の通知が見つかれば、それは existing_notification 変数に格納されます。

    つまり、ユーザーAがユーザーBをフォローしたとき、データベース内でユーザーAからユーザーBへのフォロー通知が存在するかどうかを find_by メソッドで調べているわけです。

3
if existing_notification.blank?
  • existing_notification.blank?existing_notification が空(存在しない)場合、つまり、すでに同じ通知が存在しない場合に実行されます。
4
notification = current_user.active_notifications.build(visited_id: self.id, action: 'follow')
  • current_user.active_notifications は、current_userが送信した通知の一覧を取得するためのもの。
  • build(visited_id: self.id, action: 'follow') でフォロー通知を作成する。その時以下の情報を指定している。
    1. visited_id:フォローされた側のユーザーのID。
    2. action:フォローを指定。
5
notification.save if notification.valid?
  • notification.valid? は通知がバリデーションに合格しているかどうかをチェックします。もし通知がバリデーションをクリアしている場合、save メソッドを呼び出して通知をデータベースに保存します。

投稿へのいいね通知

app/model/post.rb
  # postへのいいね通知機能
 def create_notification_favorite_post!(current_user)
   # 同じユーザーが同じ投稿に既にいいねしていないかを確認
   existing_notification = Notification.find_by(post_id: self.id, visitor_id: current_user.id, action: "favorite_post")
   
   # すでにいいねされていない場合のみ通知レコードを作成
   if existing_notification.nil? && current_user != self.user
     notification = Notification.new(
       post_id: self.id,
       visitor_id: current_user.id,
       visited_id: self.user.id,
       action: "favorite_post"
     )

     if notification.valid?
       notification.save
     end
   end
 end
解説
1
existing_notification = Notification.find_by(post_id: self.id, visitor_id: current_user.id, action: "favorite_post")
  • 既存の「いいね」通知を検索しています。Notification モデルから、post_idself.id(特定の投稿のID)、visitor_idcurrent_user.id(「いいね」をしたユーザーのID)、action'favorite_post'(「いいね」通知であることを示す)の通知を探し出します。
2
if existing_notification.nil? && current_user != self.user
  • existing_notification が存在しないかつ、current_userself.user(投稿をしたユーザーと同じでない)である場合の条件をチェックしています。同じユーザーが同じ投稿に複数回「いいね」通知を送らないようにするための条件です。
3
notification = Notification.new(post_id: self.id, visitor_id: current_user.id, visited_id: self.user.id, action: "favorite_post")
  • 新しい通知オブジェクトを生成し、特定の投稿に関連付けられ、誰が「いいね」したか、そして通知の種類が「いいね」であることを設定します。

コメントへのいいねの通知

app/model/post.rb
  # commentへのいいね通知機能
 def create_notification_favorite_comment!(current_user, comment, comment_id)
   # すでに「いいね」されているかを検索
   existing_notification = Notification.find_by(visitor_id: current_user.id, visited_id: comment.user_id, comment_id: comment_id, action: "favorite_comment")
   # いいねされていない場合のみ、通知レコード作成
   if existing_notification.blank?
     notification = current_user.active_notifications.new(
       comment_id: comment_id,
       visited_id: comment.user_id,
       action: "favorite_comment"
     )

     if notification.valid?
       notification.save
     end
   end
 end
解説
1
def create_notification_favorite_comment!(current_user, comment, comment_id)
  • このメソッドは、3つの引数を受け取ります。
    • current_user: 通知を送るユーザー(いいねをしたユーザー)。
    • comment: いいねされたコメント(commentオブジェクト)。
    • comment_id: いいねがつけられたコメントのID。
2
existing_notification = Notification.find_by(visitor_id: current_user.id, visited_id: comment.user_id, comment_id: comment_id, action: "favorite_comment")
  • すでに同じいいね通知が存在するかを検索しています。Notification モデルから、以下の条件を満たす通知を探し出します。
    • visitor_idcurrent_user.id(いいねを送ったユーザーのID)である。
    • visited_idcomment.user_id(コメントを投稿したユーザーのID)である。
    • comment_idcomment_id である。
    • action"favorite_comment" である。
3
notification = current_user.active_notifications.new(comment_id: comment_id, visited_id: comment.user_id, action: "favorite_comment")
  • 新しい通知オブジェクト notification を生成しています。このオブジェクトには、以下の情報を設定します。
    • comment_id: comment_id:いいねがつけられたコメントのID。
    • visited_id: comment.user_id:いいねがつけられたコメントを投稿したユーザーのID。
    • action: "favorite_comment":通知の種類を示すアクション。この場合、"favorite_comment" はコメントに対するいいねを通知するアクションです。

コメントへの通知

app/model/comment.rb
  # コメントが投稿された際に通知を作成するメソッド
 def create_notification_comment!(current_user, comment_id)
   # 自分以外にコメントしている人をすべて取得し、全員に通知を送る
   other_commenters_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct.pluck(:user_id)

   # 各コメントユーザーに対して通知を作成
   other_commenters_ids.each do |commenter_id|
     save_notification_comment!(current_user, comment_id, commenter_id)
   end

   # まだ誰もコメントしていない場合は、投稿者に通知を送る
   save_notification_comment!(current_user, comment_id, user_id) if other_commenters_ids.blank?
 end

 # 通知を保存するメソッド
 def save_notification_comment!(current_user, comment_id, visited_id)
   notification = current_user.active_notifications.build(
     post_id: id,
     comment_id: comment_id,
     visited_id: visited_id,
     action: 'comment'
   )

   # 自分の投稿に対するコメントの場合は、通知済みとする
   notification.is_checked = true if notification.visitor_id == notification.visited_id

   # 通知を保存(バリデーションが成功する場合のみ)
   notification.save if notification.valid?
 end
解説

機能としては以下の通りです。

  1. 自身の投稿に対してコメントがあった場合、通知がくる。
  2. 自身がコメントした投稿に、別ユーザーがコメントした場合、通知がくる。

そのため、1つの投稿に対して複数回のコメントがあるため、いいねで記載していたようなif existing_notification.blank?(通知レコードのチェック)はしません。

1
other_commenters_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct.pluck(:user_id)
  • 自分以外のユーザーが同じ投稿にコメントした場合、それらのユーザーのIDを取得します。
    • Comment.select(:user_id): データベースから user_id 列だけを選択します。
    • .where(post_id: id): 投稿IDが現在の投稿のIDと一致するコメントを絞り込みます。
    • .where.not(user_id: current_user.id): 自分のコメントを除いた他のユーザーのコメントを選択します。
    • .distinct.pluck(:user_id): 重複を削除して、ユーザーIDのリストを取得します。

コントローラでメソッドの呼び出し

各コントローラで、作成したメソッドを呼び出していきます。

フォローしたときの通知

relationships_controller.rb
class Public::RelationshipsController < ApplicationController

def create
:
  # フォロー通知を作成・保存
  @user.create_notification_follow!(current_user)
 end
: 
end

投稿にいいねされたときの通知

post_favoretes_controller.rb
class Public::PostFavoritesController < ApplicationController

 def create
  :
   if current_user != @post.user
     @post.create_notification_favorite_post!(current_user)
   end
  :
end

コメントにいいねされたときの通知

comment_favorites_controller.rb
class Public::PostFavoritesController < ApplicationController
  def create
    :
    if current_user != @post.user
      @post.create_notification_favorite_post!(current_user)
    end
  end 
  :
end

コメントへの通知

comments_controleer
class Public::CommentsController < ApplicationController
  def create
 : 
    # コメントの投稿に対する通知を作成・保存
    @post.create_notification_comment!(current_user, @comment.id)
  :
  end
end

続き

https://zenn.dev/ganmo3/articles/2f008d52ec57fc

参照

以下の記事がとても参考になります!少しアレンジしただけで導入できました!
https://qiita.com/nekojoker/items/80448944ec9aaae48d0a


長くなったので、いったんここまで。
別記事でビュー側を実装していきます!

Discussion