質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.80%

通知テーブルに一部のデータのみ保存されない。

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 403

score 65

前提・実現したいこと

「いいね」、「コメント」、「フォロー」のアクションがあった際に関連して、通知機能を実装しています。
Qiita123を参考に「いいね」と「フォロー」があった際の通知は既に上手く機能しました。

しかし「コメント」をもらった場合のみ、通知テーブルにそのデータが保存されません(もちろんコメントテーブル自体には保存されています)。エラーメッセージは表示されません。

参考にしたサイトと自身のカラム名の違いなどにも当然注意しながら実装しましたが、問題点が分かりませんでした。どなたかお知恵をお貸し頂ければ有難いです。

Schema.rb

ActiveRecord::Schema.define(version: 2020_11_24_045031) do

  create_table "comments", force: :cascade do |t|
    t.integer "user_id"
    t.integer "post_id"
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "notifications", force: :cascade do |t|
    t.integer "visitor_id", null: false
    t.integer "visited_id", null: false
    t.integer "post_id"
    t.string "action", default: "", null: false
    t.boolean "checked", default: false, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "comment_id"
    t.index ["comment_id"], name: "index_notifications_on_comment_id"
    t.index ["post_id"], name: "index_notifications_on_post_id"
    t.index ["visited_id"], name: "index_notifications_on_visited_id"
    t.index ["visitor_id"], name: "index_notifications_on_visitor_id"
  end

end

Notification.rb

class Notification < ApplicationRecord  
  belongs_to :post, optional: true
  belongs_to :comment, 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

Comment.rb

class Comment < ApplicationRecord
  validates :body, {presence: true}

  belongs_to :user
  belongs_to :post  

  has_many :notifications, dependent: :destroy

  def create_notification_comment!(current_user, comment_id)
    temp_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct
    temp_ids.each do |temp_id|
      save_notification_comment!(current_user, comment_id, temp_id['user_id'])
    end
    save_notification_comment!(current_user, comment_id, user_id) if temp_ids.blank?
  end

  def save_notification_comment!(current_user, comment_id, visited_id)
    notification = current_user.active_notifications.new(
      post_id: id,
      comment_id: comment_id,
      visited_id: visited_id,
      action: 'comment'
    )
    if notification.visitor_id == notification.visited_id
      notification.checked = true
    end
    notification.save if notification.valid?
  end
end

Routes.rb

  get 'notifications/index' => 'notifications#index'

Notification_controller.rb

class NotificationsController < ApplicationController

caches_action :index
  def index
    @notifications = @current_user.passive_notifications.includes([:visitor]).order(created_at: :desc).page(params[:page]).per(20)
    @notifications.where(checked: false).each do |notification|
      notification.update_attributes(checked: true)
    end
  end
end

Comments_controller.rb

class CommentsController < ApplicationController    
  def create
    @comment = Comment.new(comment_params)
    @post = @comment.post
    Comment.create(body: comment_params[:body], post_id: params[:post_id], user_id: @current_user.id)
    if @comment.save
       @post.create_notification_comment!(@current_user, @comment.id)
    else
      redirect_back(fallback_location: post_comments_path)
    end    
  end
end

Notifications/index.html.erb

<div class="notification">
  <h1>Notifications</h1>
</div>

<% if @notifications.exists? %>
    <div class="users-index">
        <%= render @notifications %>
    </div>
<% else %>
    <p>通知はありません</p>
<% end %>

_notification.html.erb

<div>
  <%= notification_form(notification) %>
  <span><%= "(#{time_ago_in_words(notification.created_at)} ago)" %></span>
  <br>
  <% if !@comment.nil? %>
    <p class="moderate-font text-center" style="color: #C0C0C0;"><%= @comment %></p>
  <% end %>
</div>

helpers/notifications_helper.rb

module NotificationsHelper
  def notification_form(notification)
     @visitor = notification.visitor
     @comment = nil
     your_item = link_to 'あなたの投稿', post_path(notification), style:"font-weight: bold;"
     @visitor_comment = notification.comment_id
     case notification.action
       when "follow" then
         tag.a(notification.visitor.name, href:user_path(@visitor), style:"font-weight: bold;")+"があなたをフォローしました"
       when "like" then
         tag.a(notification.visitor.name, href:user_path(@visitor), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:post_path(notification.post_id), style:"font-weight: bold;")+"にいいねしました"
       when "comment" then
           @comment = Comment.find_by(id: @visitor_comment)&.body
           tag.a(@visitor.name, href:post_path(@visitor), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:post_path(notification.post_id), style:"font-weight: bold;")+"にコメントしました"
     end
   end  
end

補足情報(FW/ツールのバージョンなど)

ruby 2.6.4p104
RubyGems 3.0.3
Rails 5.2.3

追記 post.rb

class Post < ApplicationRecord
  validates :title, {presence: true, length: {maximum: 50}}
#  validates :level, {presence: true}
#  validates :maximum, {presence: true}
#  validates :message, {presence: true, length: {maximum: 140}}
#  validates :user_id, {presence: true}

  belongs_to :user
  has_many :likes, dependent: :destroy
  has_many :comments, dependent: :destroy

  def user
    return User.find_by(id: self.user_id)
  end

  has_many :notifications, dependent: :destroy

  def create_notification_like!(current_user)
    temp = Notification.where(["visitor_id = ? and visited_id = ? and post_id = ? and action = ? ", current_user.id, user_id, id, 'like'])
    if temp.blank?
      notification = current_user.active_notifications.new(
        post_id: id,
        visited_id: user_id,
        action: 'like'
      )
      if notification.visitor_id == notification.visited_id
        notification.checked = true
      end
      notification.save if notification.valid?
    end
  end

  def create_notification_comment!(current_user, comment_id)
    temp_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct
    temp_ids.each do |temp_id|
      save_notification_comment!(current_user, comment_id, temp_id['user_id'])
    end
    save_notification_comment!(current_user, comment_id, user_id) if temp_ids.blank?
  end

  def save_notification_comment!(current_user, comment_id, visited_id)
    notification = current_user.active_notifications.new(
      post_id: id,
      comment_id: comment_id,
      visited_id: visited_id,
      action: 'comment'
    )
    return if notification.visited_id == notification.visitor_id
    if notification.visitor_id == notification.visited_id
      notification.checked = true
    end
    notification.save if notification.valid?
  end
end

Commentテーブルのデータベースの中身

[27] pry(main)> Comment.all
  Comment Load (0.4ms)  SELECT "comments".* FROM "comments"
+----+---------+---------+------+-------------------------+-------------------------+
| id | user_id | post_id | body | created_at              | updated_at              |
+----+---------+---------+------+-------------------------+-------------------------+
| 1  | 23      | 2       | hoge | 2020-11-29 09:09:27 UTC | 2020-11-29 09:09:27 UTC |
| 2  | 23      | 2       | huga | 2020-11-29 09:10:10 UTC | 2020-11-29 09:10:10 UTC |
+----+---------+---------+------+-------------------------+-------------------------+
2 rows in set
[28] pry(main)> Comment.last(2).pluck(:body)
  Comment Load (0.6ms)  SELECT  "comments".* FROM "comments" ORDER BY "comments"."id" DESC LIMIT ?  [["LIMIT", 2]]
=> ["hoge", "huga"]
[29] pry(main)>

【追記2】ストロングパラメータ変更後

comments.controller.rb内のストロングパラメータを変更しました。

    @comment = Comment.new(comment_params)
    @post = @comment.post

    Comment.create(body: comment_params[:body], post_id: params[:post_id], user_id: @current_user.id)
    if @comment.save!
       @post.create_notification_comment!(@current_user, @comment.id)
      redirect_back(fallback_location: post_comments_path) #save出来ても出来なくても、以下のルートにリダイレクト。
    else
      redirect_back(fallback_location: post_comments_path)
    end    
  end

def comment_params
  params.require(:comment).permit(:body)
end

↓ # comment_paramsの中身のみ変更。

def comment_params
  params.require(:comment).permit(:body).merge(post_id: params[:post_id], user_id: @current_user.id)
end

するとコメントを送信する度に2重に保存されるようになった(つまりcomment_paramsが機能する様になった)ので、以下の1行を消す事でコメントが通常通り保存されるようになりました。

Comment.create(body: comment_params[:body], post_id: params[:post_id], user_id: @current_user.id)

それでも通知テーブルにコメントのデータは保存されず、コマンドにて以下のエラーが出ました。

NoMethodError (undefined method `create_notification_comment!' for #<Post:0x000000000f0c16b0>
Did you mean?  create_notification_like!):

どうやらcreate_notification_comment!post.rb内に記述しないといけない(?)ようだったので、定義をcomment.rbからpost.rbに移行させました。これにより、post.rb内でのcommentに関する記述は以下の一行のみになりました。

  has_many :notifications, dependent: :destroy

するとようやく、コメントを送信後、通知テーブルにそのデータが保存されるようになりました。またビューのnotifications/indexでもしっかり通知が呼び出されました。

しかしあと2点だけ問題が出てきました。

  1. コメントを削除しても通知のデータが残ったままになる(dependent: :destroy)が機能しない。
  2. ログイン中のユーザーのコメントも、本人の通知に表示されてしまう。

2に関して、actiontrueで保存されているので、保存のされ方は問題ないかと思います。後はビュー内でも、本人には通知がいかないようにする旨の記述を加える必要があると言う事でしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • neko_daisuki

    2020/11/30 06:08

    1.コメントを削除しても通知のデータが残ったままになる(dependent: :destroy)が機能しない。

    コメントのIDは正しく保存されているように見えます。
    なので消えるはずなのですが、どうやって確認されましたか?

    コメントを削除すると以下のようなログは出ませんか?
    Notification Load (0.4ms) SELECT `notifications`.* FROM `notifications` WHERE `notifications`.`comment_id` = 2
    Notification Destroy (0.4ms) DELETE FROM `notifications` WHERE `notifications`.`id` = 1
    Notification Destroy (0.3ms) DELETE FROM `notifications` WHERE `notifications`.`id` = 2
    Notification Destroy (0.2ms) DELETE FROM `notifications` WHERE `notifications`.`id` = 3
    Comment Destroy (0.2ms) DELETE FROM `comments` WHERE `comments`.`id` = 2

    あと、notification に cache_action :index とあるのが気になります。


    2.ログイン中のユーザーのコメントも、本人の通知に表示されてしまう。

    自分自身がコメントしたと通知欄に表示されてしまうということでしょうか。
    自分以外に post にコメントしたユーザー(temp_ids)が存在する場合は、自分には通知されないと思うのですが、
    それがいない場合は post の user に通知されるようです。

    なので、post の user が current_user の場合、自分に通知されます。

    それが嫌なら validation で弾くか、
    Notification validates :visitor_id, uniqueness: { scope: [:visited_id] }

    notification.save の前に以下のようにすると良いのでは
    return if notification.visited_id == notification.visitor_id

    キャンセル

  • punchan36

    2020/11/30 14:41

    有難うございます!
    1. dependent: :destroyが機能しない点ですが、ターミナルでコメントを「Comment.destroy_all」すると以下のログが出ます。
    ---------------------------------------------------------------------------------------------
    Comment Load (7.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
    (2.3ms) begin transaction
    Comment Destroy (3.4ms) DELETE FROM "comments" WHERE "comments"."id" = ? [["id", 2]]
    (42.8ms) commit transaction
    ---------------------------------------------------------------------------------------------
    関連付けているはずのNotificationの情報は出力されず、notifications/index(ビュー上)でも「**があなたの投稿にコメントしました (** minutes ago)」と、通知の表示が残ってしまいます。コメント自体はビュー上でももちろん消えている状態です。
    また「cache_action :index」は消し忘れのコードでした。こちらを消しても上記の様に動いてしまいます。

    2. 自分自身がコメントしたと通知欄に表示されてしまう点ですが、直りました!ご助言を頂いた通り、
    return if notification.visited_id == notification.visitor_id
    を入れる事で問題が回避出来ました。重ね重ね有難うございます。

    キャンセル

  • punchan36

    2020/11/30 16:33

    修正すべき細かい点はいくつか残っておりますが、本題は解決致しました。有難うございました!
    何度もやり取りして頂いたのでベストアンサーにさせて頂きたいので、回答欄に何かコメント頂けませんでしょうか。

    キャンセル

回答 2

checkベストアンサー

+1

dependent: :destroyの件は解決できずに残念です。
さいあく、comment.rb に

after_destroy do
  self.notifications.destroy_all
end


で削除されると思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/30 21:01

    有難うございます!
    Twitterがどのようになっているか確かめてみましたが、似たような部分もありました。
    (follow→unfollow→followをした場合。最初の通知が残ったままになり、再度followしても新しい通知は来ない、等)

    取りあえず最低限の機能は果たしておりますので、細かな点は追々実装していければと思います。今回は長く付き添って頂き有難うございました!

    キャンセル

+1

Notification には実在するcommentのidが必須です

class Notification < ApplicationRecord  
  belongs_to :post, optional: true
  belongs_to :comment, optional: true


しかしそれが与えられて居ません

notification = current_user.active_notifications.new(
        post_id: id,
        visited_id: user_id,
        action: 'like'
      )


のでsaveしません

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/30 16:05

    それが原因とは思いませんが、ちと変
    「それ」⇐ Comment has_ONE notification なのでは?

    キャンセル

  • 2020/11/30 16:31

    winterboumさま
    有難うございます。そうですね。
    複数のユーザーがコメントしても通知は1通ですので、確かにhas_oneが正しいです!

    キャンセル

  • 2020/11/30 21:03

    winterboumさま
    この度はご回答有難うございました!
    別の回答者様がかなり長いやり取りに付き添って下さいましたので、そちらをベストアンサーに選ばせて頂きました。申し訳ありません。

    いつも有難うございます。また機会がありましたら宜しくお願い致します!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る