🦔

通知機能①

2024/01/18に公開

はじめに

PFもいよいよ終盤に差し掛かってきたかなと思いますが、
まだまだ時間が足りず困惑している状況です。
とまあ冗談はさておき、今回はDMの数倍苦しめられている通知機能を実装していきたいと思います。

ポリモーフィック関連

ポリモーフィック関連は、1つのモデルが複数のモデルに属する場合に使用する関連付けです。
しかし、ポリモーフィック関連を用いるとそれぞれのテーブル間の参照整合性制約を定義することが出来なくなるというアンチパターンでもあるため
使用には十分注意が必要です。

AIさんによると,,,

ポリモーフィック関連とは、オブジェクト指向プログラミングにおいて、1つのクラスが複数の異なる型として使用できる性質のことを指します。具体的には、親クラスやインターフェースなどの共通の型を持つ複数の子クラスがあり、この親クラスやインターフェースを使用する箇所では適切な子クラスを選択して利用することができるという特徴を持ちます。

ポリモーフィック関連を実現するためには、特殊な構文やキーワードを使わずに、継承や実装などの基本的なオブジェクト指向の概念を適切に利用します。親クラスやインターフェースを定義し、それを実装する子クラスを作成します。そして、適切な子クラスを選択して利用する際には、型チェックや型キャストなどを使用することで、ポリモーフィックな動作を実現します。

例えば、動物を表すAnimalという親クラスがあり、その子クラスとして犬を表すDogクラスと猫を表すCatクラスがある場合、Animal型の変数にDogクラスやCatクラスのインスタンスを代入することができます。そして、Animal型の変数を使用する箇所では、実際のインスタンスの型に応じて適切なメソッドが呼ばれるようになります。

実装

モデルを作成します。

rails g model Notification

次にマイグレーションファイルを編集していきます。

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 :post_comment_id
      t.integer :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を忘れずに

t.string :action, null: false, default: ''の解説

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

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

t.boolean :is_checked, null: false, default: falseの解説

  • t.boolean:
    trueかfalseのどちらかを格納するためのブール型にします。

  • null: false
    trueかfalseのどちらかが入るのでnullにできないようにします。

  • default: false
    既読になったらtrueにしたいので、デフォルトはfalseです。

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

  belongs_to :post, optional: true
  belongs_to :post_comment, optional: true
  belongs_to :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:``
関連付けられたデータがなくても大丈夫、という設定。

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

フォロー

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

def create_notification_follow!(current_user)

  • ここでの!は「バンメソッド」とも呼ばれ、使うのに注意が必要だよ!という意味です。つけてもつけなくても問題はありません。

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

  • existing_notification は変数で、通知がすでに存在する場合にその通知を格納します。
  • Notification.find_by(...) は、データベースから通知を探し出すためのメソッドです。
  • このコードを通じて、特定のユーザー(current_user)から別のユーザー(self、フォローされたユーザー)に対するフォロー通知がすでに存在するかどうかを調べています。もし既存の通知が見つかれば、それは existing_notification 変数に格納されます。

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

if existing_notification.blank?

  • existing_notification.blank? は existing_notification が空(存在しない)場合、つまり、すでに同じ通知が存在しない場合に実行されます。

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') でフォロー通知を作成する。その時以下の情報を指定している。
    visited_id:フォローされた側のユーザーのID。
    action:フォローを指定。

notification.save if notification.valid?

  • notification.valid? は通知がバリデーションに合格しているかどうかをチェックします。
    もし通知がバリデーションをクリアしている場合、save メソッドを呼び出して通知をデータベースに保存します。

投稿へのいいね通知、コメント通知

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

   # コメントが投稿された際に通知を作成するメソッド
 def create_notification_post_comment!(current_user, post_comment_id)
   # 自分以外にコメントしている人をすべて取得し、全員に通知を送る
   other_post_commenters_ids = PostComment.where(post_id: id).where.not(user_id: current_user.id).distinct.pluck(:user_id)
   # 各コメントユーザーに対して通知を作成
   other_post_commenters_ids.each do |post_commenter_id|
     save_notification_post_comment!(current_user, post_comment_id, post_commenter_id)
   end

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

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

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

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

投稿に対するいいね

existing_notification = Notification.find_by(post_id: self.id, visitor_id: current_user.id, action: "favorite_post")

  • 既存の「いいね」通知を検索しています。Notification モデルから、post_id が self.id(特定の投稿のID)、visitor_id が current_user.id(「いいね」をしたユーザーのID)、action が 'favorite_post'(「いいね」通知であることを示す)の通知を探し出します。

if existing_notification.nil? && current_user != self.user

  • existing_notification が存在しないかつ、current_user が self.user(投稿をしたユーザーと同じでない)である場合の条件をチェックしています。同じユーザーが同じ投稿に複数回「いいね」通知を送らないようにするための条件です。

notification = Notification.new(post_id: self.id, visitor_id: current_user.id, visited_id: self.user.id, action: "favorite_post")

  • 新しい通知オブジェクトを生成し、特定の投稿に関連付けられ、誰が「いいね」したか、そして通知の種類が「いいね」であることを設定します。

コメントへの通知

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

自身の投稿に対してコメントがあった場合、通知がくる。
自身がコメントした投稿に、別ユーザーがコメントした場合、通知がくる。
そのため、1つの投稿に対して複数回のコメントがあるため、いいねで記載していたようなif existing_notification.blank?(通知レコードのチェック)はしません。
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

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

favoretes_controller.rb
class Public::FavoritesController < ApplicationController

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

コメントへの通知

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

Discussion