Rails チャット機能

## はじめに
[前回、Railsアプリケーションにマッチング機能を実装しました。](https://qiita.com/gobtktk/items/5caf074209faf31cae4a)今回は、マッチング成立後のユーザー同士のチャット機能を実装していきたいと思います。

### 実装環境
Rails7.0.2
Ruby3.1.0

## メッセージ機能の仕様
ユーザー同士が、「お気に入り！」評価をしている場合（マッチング成立時）に限り、チャットができるようにします。
ユーザーは複数のチャットに参加することができます。またチャットは、２人でするものとします。

## 実装の流れ
1. マッチング成立ユーザーページの実装（viewとcontroller)
1. チャットルーム用のテーブル(chat_rooms)を作成。
1. 中間テーブル（chat_room_users）を作成
1. ルーティングの作成
1. コントローラーの作成


## マッチング成立後、ユーザーページの実装（viewとcontroller)
### 1.ルーティングの設定
~~~ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users
  root "home#index"
  resources :users, only: [:show,:index]
  resources :likes, only: [:create]
  #ここから
  resources :matching, only: [:index]
  #ここまで追記
end
~~~
![スクリーンショット 2022-04-20 23.49.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/d111c43a-a57b-b260-9c49-d8f50a85cb9f.png)

### 2.コントローラーの設定

アクションの処理を記述する前に実現したいことをまとめておきます。
マッチング成立条件＝
**自分がお気に入り評価したユーザーが自分をお気に入り評価している状態。**
1. 自分をお気に入り評価してくれたユーザーを取得。
1. 1.のうち、自分がお気に入り評価したユーザーを取得（マッチング成立ユーザー）

~~~terminal
$ rails g controller matching
~~~
~~~ruby:matching_controller.rb
 class MatchingController < ApplicationController
    before_action :authenticate_user!

    def index
        #ログインユーザーに対して「お気に入り！」の評価をしたユーザーのidを配列で取得。
        got_like_users_ids = Like.where(to_user_id: current_user.id, status: "like").pluck(:from_user_id)
        #idが「１」のユーザーがidが「２」、「３」の２人のユーザーから「お気に入り！」の評価を受けている場合の戻り値は以下のようにな配列となります。
        #=>[2,3]

        #上記で取得したユーザーのうち、ログインユーザーからも「お気に入り！」の評価をしたユーザー情報を取得して、ブロックの結果を要素に持つ配列を新たに作成。
        @mathing_users=Like.where(to_user_id: got_like_users_ids, from_user_id: current_user.id, status: "like").map do |like|
            like.to_user
        end
        @user=User.find(current_user.id)
    end

end

~~~
各処理について確認します。
~~~ruby
got_like_users_ids = Like.where(to_user_id: current_user.id, status: "like").pluck(:from_user_id)
~~~
Likeモデルからwhereメソッドを使用して、引数の條件に一致するデータを取得しています。
具体的には、
1. ログインユーザーに「like」と評価したユーザーに関するデータをLikeモデルから配列で取得。
1. 取得した配列のうち、procメソッドで「from_user_id」のカラム情報のみを取得。

~~~ruby
@mathing_users=Like.where(to_user_id: got_like_users_ids, from_user_id: current_user.id, status: "like").map do |like|
            like.to_user
end
~~~
ログインユーザーとマッチングが成功したユーザーの情報を取得しています。
具体的には、
1. ログインユーザーへ「like」評価をしたユーザーのうち、ログインユーザーからも「like」評価をしている情報をLikeモデルから配列で取得。
1. mapメソッドにより、取得した配列から要素を取り出し、ブロック処理を実効。実効した結果を要素に持つ新しい配列を作成。

参考:[4.1.1 belongs_toで追加されるメソッド](https://railsguides.jp/association_basics.html#belongs-to%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
参考:[pluck](https://railsdoc.com/page/model_pluck)
参考:[where](https://railsdoc.com/page/model_where)
参考:[【Ruby】 mapメソッドの基礎から応用の使い方まとめ](https://pikawaka.com/ruby/map)
### 3.Viewの設定
~~~html:matching/index.html.erb
<%= render "partial/navbar" %>
<p>マッチング中のお相手 <%= @matching_users.size %>人</p>
<% @mathing_users.each do |matching_user| %>
    <ul>
        <li>
            <%= link_to matching_user.username,"#" %>
        </li>
    </ul>
<% end %>
~~~
リンクについては、この後設定しますので、一旦ダミーリンクを貼っておきます。
![スクリーンショット 2022-04-23 12.56.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/825a1deb-746a-bd3c-5aa2-19b480e1d1be.png)


マッチング成立ユーザーの取得に成功しています。
このように表示されていれば、大丈夫です。

## チャットルーム用のテーブル(chat_rooms)を作成。
マイグレーションファイルの作成
~~~terminal
$ rails g model ChatRooms
~~~
マイグレーションファイルの確認
~~~ruby:2022xxxxxx_create_chat_rooms.rb
class CreateChatRooms < ActiveRecord::Migration[7.0]
  def change
    create_table :chat_rooms do |t|

      t.timestamps
    end
  end
end
~~~
データベースをマイグレート
~~~terminal
$ rails db:migrate
~~~
データベース構造の確認
~~~ruby:schema.rb
ActiveRecord::Schema[7.0].define(version: 2022_04_20_111740) do
 
  #マイグレーション実効により、ここから
  create_table "chat_rooms", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
  #ここまで追記される。

  create_table "likes", force: :cascade do |t|
    t.integer "to_user_id", null: false
    t.integer "from_user_id", null: false
    t.integer "status", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["from_user_id"], name: "index_likes_on_from_user_id"
    t.index ["to_user_id"], name: "index_likes_on_to_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "username", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  add_foreign_key "likes", "users", column: "from_user_id"
  add_foreign_key "likes", "users", column: "to_user_id"
end

~~~
## ER図
前回と同様にモデル同士のアソシエーションを整理するべく、ER図を作成します。
先ほど新たに作成した「chat_rooms」テーブルと「users」テーブルの関連性については、次の通りです。
一人のユーザーは、複数のチャットルームを持ち、
一つのチャットルームでは、複数人のユーザー（２人）を持ちます。
つまり、二つのモデルは、**多対多**の関係性にあることがわかります。

![スクリーンショット 2022-04-20 19.37.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/68bbcf3c-fba2-4770-0376-f4116d84f2a3.png)

多対多の関係性である場合は、**中間テーブル**を用意して、その関係性を表します。
#### ◯多対多の場合に、中間テーブルが必要な理由

中間テーブルを使わないと、、、、、

- 外部キーを記録するカラム数が多くなる。（１カラムにつき、１データのため）
- 使われないカラムが出てくる。(null値が入るカラムができる。)

上記のような、DB設計のアンチパターンになってしまいます。

参考:[【Rails】 アソシエーションを図解形式で徹底的に理解しよう！](https://pikawaka.com/rails/association#中間テーブルとthroughオプション)

## 中間テーブル（chat_room_users）を作成
マイグレーションファイルの作成
~~~terminal
$ rails g model ChatRoomUsers 
~~~
マイグレーションファイルを編集
~~~ruby:2022xxxxxxxxx_create_chat_room_users
class CreateChatRoomUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :chat_room_users do |t|
      #外部キー制約のためここから
      t.references :chat_room, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      #ここまを追加
      t.timestamps
    end
  end
end
~~~
データベースをマイグレート
~~~terminal
$ rails db:migrate
~~~
データベースの構造を確認
~~~ruby:schema.rb
ActiveRecord::Schema[7.0].define(version: 2022_04_20_113225) do
  #ここから
  create_table "chat_room_users", force: :cascade do |t|
    t.integer "chat_room_id", null: false
    t.integer "user_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["chat_room_id"], name: "index_chat_room_users_on_chat_room_id"
    t.index ["user_id"], name: "index_chat_room_users_on_user_id"
  end
  #ここまでが追加
  create_table "chat_rooms", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "likes", force: :cascade do |t|
    t.integer "to_user_id", null: false
    t.integer "from_user_id", null: false
    t.integer "status", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["from_user_id"], name: "index_likes_on_from_user_id"
    t.index ["to_user_id"], name: "index_likes_on_to_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "username", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end
  #ここから
  add_foreign_key "chat_room_users", "chat_rooms"
  add_foreign_key "chat_room_users", "users"
  #ここまでが追加
  add_foreign_key "likes", "users", column: "from_user_id"
  add_foreign_key "likes", "users", column: "to_user_id"
end

~~~
### ER図
![スクリーンショット 2022-04-20 20.09.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/b17ccb3c-7f15-1339-1754-8412ee1dec24.png)

中間テーブルを作成し、外部キー情報を持つことにより、
usersとchat_roomsテーブルに外部キーを記録するための不要なカラムが追加されずに済みます。

### モデルの設定
~~~ruby:user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
  #ここから
  has_many :chat_room_users
  has_many :chat_rooms, through: :chat_room_users 
  #ここまで追記

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
~~~
~~~ruby:chat_room.rb
class ChatRoom < ApplicationRecord
    has_many :chat_room_users
    has_many :users, through: :chat_room_users 
end
~~~
~~~ruby:chat_room_user.rb
class ChatRoomUser < ApplicationRecord
    belongs_to :chat_room
    belongs_to :user
end
~~~

modelでhas_many,belongs_toの設定を行うことで、関連するオブジェクトを取得することのできる、associationメソッドを使用できるようになります。

参考:[4.1 belongs_to関連付けの詳細](https://railsguides.jp/association_basics.html#belongs-to%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

## ルーティングの作成
~~~ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users
  root "home#index"
  resources :users, only: [:show,:index]
  resources :likes, only: [:create]
  resources :matching, only: [:index]
  #ここから
  resources :chat_rooms, only: [:create, :show]
  #ここまで追記
end
~~~
## ChatRoom作成と遷移
”matching/index.html.erb”でマッチングしているユーザーのリンクをクリックすると、チャットルームに遷移するように実装していきます。

### Createアクションの実装(chat_roomsコントローラー)
~~~terminal
$ rails g controller chat_rooms
~~~
まずは、createアクションから実装していきます。
~~~ruby:chat_rooms_controller.rb
 def create
        #ログインユーザーの所属するチャットルームを全件配列で取得。
        current_users_chat_rooms = ChatRoomUser.where(user_id: current_user.id).map do |chat_room_user| 
            chat_room_user.chat_room   
        end
        # パラメーターで与えられたuser_idのユーザーとログインユーザーが所属するチャットルームを配列で取得し、インデックスで要素を取り出す。
        chat_room = ChatRoomUser.where(chat_room_id: current_users_chat_rooms,user_id: params[:user_id])[0].chat_room  
        #条件に合うチャットルームが存在しない場合は、チャットルームを新規作成。
        if chat_room.blank? 
            chat_room=ChatRoom.create
            ChatRoomUser.create(user_id: current_user.id, chat_room_id: chat_room.id)
            ChatRoomUser.create(user_id: params[:user_id], chat_room_id: chat_room.id)    
        end
        #chat_rooms/show.html.erbへ遷移
        redirect_to chat_room_path(chat_room)
end

    def show
    end
~~~
### show.html.erbの作成
~~~html:chat_rooms/show.html.erb
<p>Chatroomへようこそ!</p>
~~~
![go_to_chatroom.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/83da921d-65af-57a2-2f27-ed37bed5c661.gif)

Chatroomページヘ遷移すれば成功です。

## メッセージ機能の実装
いよいよ本題のメッセージ機能の実装を行います。

### 1.chat_messagesテーブルの作成
~~~terminal
$ rails g model ChatMessages
~~~
~~~ruby:2022xxxxxxx_create_chat_messages.rb
class CreateChatMessages < ActiveRecord::Migration[7.0]
  def change
    create_table :chat_messages do |t|
      #ここから
      t.references :chat_room, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.text :content
      #ここまでを追記
      t.timestamps
    end
  end
end
~~~
~~~terminal
$ rails db:migrate
~~~
~~~ruby:schema.rb
ActiveRecord::Schema[7.0].define(version: 2022_04_23_051410) do
  #ここから
  create_table "chat_messages", force: :cascade do |t|
    t.integer "chat_room_id", null: false
    t.integer "user_id", null: false
    t.text "content"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["chat_room_id"], name: "index_chat_messages_on_chat_room_id"
    t.index ["user_id"], name: "index_chat_messages_on_user_id"
  end
  #ここまでが追記される
  create_table "chat_room_users", force: :cascade do |t|
    t.integer "chat_room_id", null: false
    t.integer "user_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["chat_room_id"], name: "index_chat_room_users_on_chat_room_id"
    t.index ["user_id"], name: "index_chat_room_users_on_user_id"
  end

  create_table "chat_rooms", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "likes", force: :cascade do |t|
    t.integer "to_user_id", null: false
    t.integer "from_user_id", null: false
    t.integer "status", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["from_user_id"], name: "index_likes_on_from_user_id"
    t.index ["to_user_id"], name: "index_likes_on_to_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "username", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end
  #ここから
  add_foreign_key "chat_messages", "chat_rooms"
  add_foreign_key "chat_messages", "users"
  #ここまでが追記される
  add_foreign_key "chat_room_users", "chat_rooms"
  add_foreign_key "chat_room_users", "users"
  add_foreign_key "likes", "users", column: "from_user_id"
  add_foreign_key "likes", "users", column: "to_user_id"
end
~~~

### 2.モデルの関連づけ
モデルの関係性は、次のとおりです。
- ChatRoomとChatMessageは１対多
- UserとChatMessageは１対多
~~~ruby:chat_message.rb
class ChatMessage < ApplicationRecord
    belongs_to :chat_room
    belongs_to :user
end
~~~
~~~ruby:chat_room.rb
class ChatRoom < ApplicationRecord
    has_many :chat_room_users
    has_many :users,through: :chat_room_users
    #ここから
    has_many :chat_messages
    #ここまで追記
end
~~~
~~~ruby:user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
  has_many :chat_room_users
  has_many :chat_rooms, through: :chat_room_users 
  #ここから
  has_many :chat_messages
  #ここまで追記
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

~~~


### 3.ER図
モデルの関係性は、次のとおりです。
- ChatRoomとChatMessageは１対多
- UserとChatMessageは１対多

![スクリーンショット 2022-04-23 14.40.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/fdabbb13-a1ef-9749-a2d2-59d29f8eab41.png)

### 4.ルーティングの設定
~~~ruby:rutes.rb
Rails.application.routes.draw do
  devise_for :users
  root "home#index"
  resources :users, only: [:index, :show]
  resources :likes, only: [:create]
  resources :matching, only: [:index]
  resources :chat_rooms, only: [:create, :show]
  #ここから
  resources :chat_messages, only: [:create]
  #ここまで追加
end
~~~
### 5-1. コントローラーの編集（chat_rooms_controller）
showアクションを実装していきます。
ここでチャットルームのメッセージを表示したいと思います。
メッセージを表示する際は、「誰が」「いつ」送信したメッセージなのかも併せて表示できるようにします。
~~~ruby:chat_rooms_controller.rb
class ChatRoomsController < ApplicationController
    def create
        current_users_chat_rooms = ChatRoomUser.where(user_id: current_user.id).map do |chat_room_user| 
            chat_room_user.chat_room   
        end
        chat_room = ChatRoomUser.where(chat_room_id: current_users_chat_rooms,user_id: params[:user_id])[0].chat_room  
        if chat_room.blank? 
            chat_room=ChatRoom.create
            ChatRoomUser.create(user_id: current_user.id, chat_room_id: chat_room.id)
            ChatRoomUser.create(user_id: params[:user_id], chat_room_id: chat_room.id)    
        end
        redirect_to chat_room_path(chat_room)
    end

    def show
        #ここから編集

        #フォームに渡すために、モデルのインスタンスを作成。
        @chat_message=ChatMessage.new
        #受け取ったクエリパラメータでチャットルームオブジェクトを取得。
        @chat_room=ChatRoom.find(params[:id])
        #表示するチャットルーム内でのメッセージを全件配列で取得。
        @chat_messages=ChatMessage.where(chat_room: @chat_room)
        #チャット相手ユーザーの取得。
        @chat_room_user=@chat_room.chat_room_users.where.not(user_id: current_user.id)[0].user

        #ここまで編集
    end
end

~~~


### 5-2. コントローラーの作成（chat_messages_controller）
~~~terminal
$ rails g controller chat_messages
~~~
メッセージを作成するためのcreateアクションを定義します。
~~~ruby:chat_messages_controller.rb
class ChatMessagesController < ApplicationController
    def create
        #フォームから受け取った値でチャットルームオブジェクトを取得
        @chat_room=ChatRoom.find(params[:chat_message][:chat_room_id])
        #フォームから受け取った値で、チャットメッセージオブジェクトを作成
        @chat_message=ChatMessage.new(user_id: current_user.id, chat_room_id: @chat_room.id, content: params[:chat_message][:content])
        #保存に成功したら、フラッシュメッセージを表示し、チャットルームへリダイレクトする。
        if @chat_message.save
            flash[:notice]="メッセージの送信に成功しました。"
            redirect_to chat_room_path(@chat_room)
        #保存に失敗した場合は、フラッシュメッセージ表示し、チャットルームへリダイレクトする。
        else
            flash[:alert]="メッセージの送信に失敗しました。"
            redirect_to chat_room_path(@chat_room)
        end
    end
end
~~~

### 6.chat_rooms/show.html.erbの編集
~~~html:chat_rooms/show.html.erb
<%= render "partial/navbar" %>
<p>Chatroomへようこそ!</p>

<p>お相手：<%= @chat_room_user.username %>さん</p>

<% @chat_messages.each do |chat_message| %>
        <hr>
        <div>
            <li>内容：<%= chat_message.content %></li>
            <li><%= chat_message.created_at %>に送信済み</li>
            <li><%= chat_message.user.username %>さんのメッセージ</li>
        </div>
        <hr>
<% end %>

<%= form_with model: @chat_message do |f| %>
    <%= f.hidden_field :chat_room_id, value: @chat_room.id %>
    <%= f.text_area :content %>
    <%= f.submit %>
<% end %>
~~~
フォームでは、:model引数に空のインスタンス(@chat_message)を渡しています。
そうすることで、フォームをオブジェクトに結び付けることができ、次のことが自動で行われます。
- @chat_messageは空のオブジェクトなので、createアクションが呼び出される。
- フォームのフィールド名は、chat_message[...]という形でスコープされ、params[:chat_message]がすべてのフィールドの値を含むハッシュになる


また、フォームビルダーでは、hidden_fieldを使ってでチャットルームのidを渡しています。
こうすることで、どのチャットルーム内でのメッセージなのかの情報をcreateアクションに渡すことができます。
参考:[2 モデルオブジェクトを扱う](https://railsguides.jp/form_helpers.html#%E3%83%A2%E3%83%87%E3%83%AB%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E6%89%B1%E3%81%86)

![メッセージ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/2ead263e-da79-d3a4-5461-b32925b2e621.gif)
チャットルーム内のフォームでメッセージの送信。表示ができていれば、成功です。



## 参考
[Railsガイド](http://railman.net/railsguides/4.2/association_basics.html#has-many-through%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)
[【Rails】 アソシエーションを図解形式で徹底的に理解しよう！](https://pikawaka.com/rails/association#中間テーブルとthroughオプション)
[4.1.1 belongs_toで追加されるメソッド](https://railsguides.jp/association_basics.html#belongs-to%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
[pluck](https://railsdoc.com/page/model_pluck)
[where](https://railsdoc.com/page/model_where)
[【Ruby】 mapメソッドの基礎から応用の使い方まとめ](https://pikawaka.com/ruby/map)