Rails DM機能と非同期でメッセージの送受信を実装する方法

#なにこれ
DM機能とメッセージを非同期で送受信するための実装手順をまとめました。
備忘録と言語化するために書いてます！

#前提条件
・Usersテーブルが作成済みであること

 ##全体の流れ
1，テーブル作成
2，モデルでアソシエーションを組む
3，usersコントローラーを編集

#テーブル設計
今回は合計4つのテーブルが必要になります。
ユーザー情報を保存するUsersテーブル
誰がDMのどのルームに参加したかuser_idとroom_idを管理するEntriesテーブル
ルーム自体を管理するroomsテーブル
どのルームで誰がメッセージを送ったのか管理するmessagesテーブル
文字だけの説明じゃなくて、
ご自身でテーブルを紙に書き出して見ると分かりやすいと思います！

#Usersテーブル
今回の場合はnicknameカラムとavatarカラムを追加してますが、
ここは個人で編集して下さい。
avatarは必要なさそうならコメントアウトすることをオススメします。
#
```ruby:マイグレーションファイル
  def change
    add_column :users, :nickname, :string
    add_column :users, :avatar, :string
  end
```

#Entriesテーブル
どのユーザーがどのルームに入ったかを管理するのがEntriesテーブルの役割です！
user_idとroom_idに外部キー制約をかけます。
#
```ruby:マイグレーションファイル
class CreateEntries < ActiveRecord::Migration[5.2]
  def change
    create_table :entries do |t|
      t.references :user, foreign_key: true
      t.references :room, foreign_key: true
      t.timestamps
    end
  end
end
```

#Roomsテーブル
どのルームか管理するのがRoomsテーブルの役割です。
このテーブルにカラムは必要ありません！

#
```ruby:マイグレーションファイル
class CreateRooms < ActiveRecord::Migration[5.2]
  def change
    create_table :rooms do |t|
      t.timestamps
    end
  end
end
```

#Messagesテーブル
誰がどのルームでどんなメッセージを送ったのか管理するのがMessagesテーブルの役割！
Entriesテーブルと同じくuser_idとroom_idに外部キー制約をかけます。
#
```ruby:マイグレーションファイル
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.references :user, foreign_key: true
      t.references :room, foreign_key: true
      t.text :message, null: false
      t.timestamps
    end
  end
end
```

#アソシエーションを組む

#userモデル
1人のユーザーは複数のメッセージ、エントリー、ルームに加入してるので
全てhas_manyになります。
`has_many :rooms, through: :entries`は、DMしてるユーザー一覧を作るために必要です。
entriesテーブルを経由してroomsの情報をかき集めてきます！

#
```ruby:user.rb
  has_many :messages
  has_many :entries
  has_many :rooms, through: :entries
```

#entryモデル
ユーザーの情報とルームはレコードごとに被ることはないように一意の制約をかけます。

Entriesテーブル使用中のレコードの一例）
Entry_id[0] user_id[1] room_id[0]
Entry_id[1] user_id[3] room_id[0]

#
```ruby:entry.rb
    belongs_to :user
    belongs_to :room

    validates :room_id, uniqueness: { scope: :user_id }
```

#roomモデル
Roomsは複数のメッセージとエントリー情報を持つのでhas_manyになります！
`has_many :users, through: :entries`はuserモデルにもあった、一覧表示をするのに必要です。

#
```ruby:room.rb
    has_many :messages
    has_many :entries
    has_many :users, through: :entries
```

#messageモデル
#
```ruby:message.rb
    belongs_to :user
    belongs_to :room

    validates :message, presence: true

```
 #ルーティングの設定
全体の流れ
1，ユーザーの詳細ページからDMルームに飛ぶ
2，ルームは参加と一覧表示ができる
3，メッセージは投稿、編集、削除ができる

#
```ruby:routes.rb
  resources :users, only: [:show]
  resources :rooms, only: [:index, :create:, :show]
  resources :messages, only: [:create, :edit, :update, :destroy]
```

#usersコントローラーの編集
以下の記述になります。上から順番に区切って説明します。

#
```ruby:users_controller.rb
  def show
@user = User.find(params[:id])
    if user_signed_in?
      @currentUserEntry = Entry.where(user_id: current_user.id)
      @userEntry = Entry.where(user_id: @user.id)
      unless @user.id == current_user.id
        @currentUserEntry.each do |cu|
          @userEntry.each do |u|
            if cu.room_id == u.room_id
              @haveRoom = true
              @roomId = cu.room_id
            end
          end
        end
        unless @haveRoom
          @room = Room.new
          @entry = Entry.new
        end
      end
    end
  end
```

#
```ruby:users_controller.rb
@user = User.find(params[:id])
    if user_signed_in?
      @currentUserEntry = Entry.where(user_id: current_user.id)
      @userEntry = Entry.where(user_id: @user.id)
      unless @user.id == current_user.id
```
`@user = User.find(params[:id])`でパラメーターから送られたuser_idを@userに代入
`if user_signed_in?`でサインイン済みか判断。
`@currentUserEntry=Entry.where(user_id: current_user.id)`でcurrent_userが
既にルームに参加してるか判断。
`@userEntry=Entry.where(user_id: @user.id)`でユーザー詳細ページの表示しているユーザーがルームに参加してるか判断。
`unless @user.id == current_user.id`で@userとcurrent_userが違うユーザーであることを確認。

ここまで理解できてますか？
やってる内容は至ってシンプルです。
current_userと@userが部屋に入ってるかの情報を出してます。
次の内容が少し複雑ですが、シンプルに考えてみてほしいです！

#
```
        @currentUserEntry.each do |cu|
          @userEntry.each do |u|
            if cu.room_id == u.room_id
              @haveRoom = true
              @roomId = cu.room_id
            end
          end
        end
```

`@currentUserEntry.each do |cu|`でcurrent_userが参加してる全てのルームidを出力します。
`@userEntry.each do |u|`で@userが参加してる全てのルームidを出力します！
`if cu.room_id == u.room_id`でcurrent_userの参加してるroom_idと@userのroom_idで一致するものがあるのか判断します。

画像の例を出しておきます！

<img width="442" alt="Entriesテーブル.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629015/8d86f697-6198-d4bc-5771-0c18cfae7852.png">


このテーブルの中に一致する内容があったらそこで処理が終わります。
既にルームが作成されてるので。
もし作成されてないなら、新規にルームを作成する処理に分岐します！

もし部屋が作成されてなかったら、以下の処理をします。
`@haveRoom = true`を代入
`@roomId = cu.room_id`を代入します。
@haveRoomは既にルームがあるよ。という意味の変数です。
@roomIdは後ほど記述しますが、ルームにアクセスするための変数です。

#
```ruby:users_controller.rb
        unless @haveRoom
          @room = Room.new
          @entry = Entry.new
        end
```
@habeRoomに値がなかったら、新規作成するための変数を作成します。
form_forで@roomと@entryのの情報を送るためです。

#ビューの編集
次はビューの編集をします。ここから必要な部分の一部抜粋になります
ユーザー詳細ページからDMルームに飛ぶための部分テンプレートを用意してrenderします

#
```ruby:/users/show.html.haml
= render "contact"
```

DMルームに飛ぶための記述です。
こちらも区切って解説します！
#
```ruby:/users/_contact.html.haml
- if user_signed_in?
    - unless @user.id == current_user.id
        - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)
        - else
            = form_with(model:@room, local: true) do |f|
                = fields_for @entry do |e|
                    = e.hidden_field :user_id, value: @user.id
                = f.submit "ダイレクトメッセージ"
```

#
```ruby:/users/_contact.html.haml
- if user_signed_in?
    - unless @user.id == current_user.id
        - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)
```
1行目でサインインしてるか確認
2行目で@userとcurrent_userのidが違うことを確認
3行目でusers_controllerのshowアクションで作成した変数@haveRoomがtrueであることを確認。
4行目にルームに参加するためのリンクを表示してます！

#
```ruby:/users/_contact.html.haml
     - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)
        - else
            = form_with(model:@room, local: true) do |f|
                = fields_for @entry do |e|
                    = e.hidden_field :user_id, value: @user.id
                = f.submit "ダイレクトメッセージ"
```
@haveRoomがfalseだった場合（値が入ってない場合）の流れです。
form_withでデータを送信します。モデルは@roomで、local: trueで画面遷移するように命令します。
rooms_controllerのcreateアクションを呼び出すためにこの記述をしてます！
rooms_controllerは次に解説します。

fields_forという珍しいメソッドが出てきました！
簡単に言うと、
form_with内で異なるモデルを編集することができるようになるもの。
 今回はroomテーブルとentryテーブルが１対多の関係の時に
entryテーブルに値を保存できるようにするために使用した。

つまり、ここでやりたいことはEntriesテーブルにuser_idとroom_idを追加したい！！
そういうことです。

#roomsコントローラーの編集
全体の記述は以下のようになります。上から順番に区切って解説します。
#
```ruby:rooms_controller.rb
class RoomsController < ApplicationController
    before_action :authenticate_user!

    def index
        @rooms = current_user.rooms.includes(:messages).order("messages.created_at desc")
    end

    def create
        @room = Room.create
        @joinCurrentUser = Entry.create(user_id: current_user.id, room_id: @room.id)
        @joinUser = Entry.create(join_room_params)
        @first_message = @room.messages.create(user_id: current_user.id, message: "初めまして！")
        redirect_to room_path(@room.id)
    end

    def show
        @room = Room.find(params[:id])
        if Entry.where(user_id: current_user.id, room_id: @room.id).present?
            @messages = @room.messages.includes(:user).order("created_at asc")
            @message = Message.new
            @entries = @room.entries
        else
            redirect_back(fallback_location: root_path)
        end
    end

    private
    def join_room_params
        params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id)
    end

end
```
def indexは参加中のDMルームの一覧表示をするための記述です。

#
```ruby:rooms_controller.rb
    def create
        @room = Room.create
        @joinCurrentUser = Entry.create(user_id: current_user.id, room_id: @room.id)
        @joinUser = Entry.create(join_room_params)
        @first_message = @room.messages.create(user_id: current_user.id, message: "初めまして！")
        redirect_to room_path(@room.id)
    end
```
変数@roomを作成。
変数@joinCurrentUserでEntriesテーブルにuser_id(current_user.id)と参加するroom_idの情報を入れます。
変数@joinUserで参加するユーザー（今回の場合だと/users/showで表示していたuser_id）の情報を入れます。
細かい記述は省略します！
@first_messageは初めの一言を自動的に入力させてます。
作成したroom_idにリダイレクトさせます。

#
```ruby:rooms_controller.rb
    def show
        @room = Room.find(params[:id])
        if Entry.where(user_id: current_user.id, room_id: @room.id).present?
            @messages = @room.messages.includes(:user).order("created_at asc")
            @message = Message.new
            @entries = @room.entries
        else
            redirect_back(fallback_location: root_path)
        end
    end
```
@roomにパラメーターから送られたroom_idを代入
3行目でEntriesテーブルに情報があるか確認
4行目で@roomの全てのメッセージを取得
5行目で新しくメッセージを作成できるようにする@messageを定義
@entriesに@roomに参加してる（Entriesテーブルでヒットした情報、またはエントリーしてる）ユーザーを取得

次はルームのビューを作成します！

#
```ruby:/rooms/show/1/.html.haml
.div#chat_area
  = render "chat_area"
.div#chat_form
  = render "chat_form"
```
チャット全体表示と投稿フォームの部分テンプレートを表示してます！

#
```ruby:/rooms/_chat_area.html.haml
.left-button
%h4.rooms-title 気になる同士
- @entries.each do |e|
  .user-name
    %strong
      = image_tag "#{e.user.avatar}", class:"avatar"
      %a.rooms-user-link{href: "/users/#{e.user.id}"}
        = e.user.nickname
        さん
%hr
.chats
  .chat__scroll
    - if @messages.present?
      - @messages.each do |m|
        %div{id: "message_#{m.id}"}
          .chatbox
            .chat-face1
              = image_tag "#{m.user.avatar}", class:"avatar"
              .chat-hukidashi
              = m.user.nickname
              %br
              = m.message
              %br
              = m.created_at.strftime("%Y-%m-%d %H:%M")
              = form_with(model: @message, url: edit_message_path(m.id), remote: true, method: :get) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "編集"
              = form_with(model: @message, url: message_path(m.id), remote: true, method: :delete) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "削除"
```
上から区切って説明します！
#
```ruby:/rooms/_chat_area.html.haml
.left-button
%h4.rooms-title 気になる同士
- @entries.each do |e|
  .user-name
    %strong
      = image_tag "#{e.user.avatar}", class:"avatar"
      %a.rooms-user-link{href: "/users/#{e.user.id}"}
        = e.user.nickname
        さん
```
先程@entriesに代入したユーザー情報を表示させてます。
#
```ruby:/rooms/_chat_area.html.haml
.chats
  .chat__scroll
    - if @messages.present?
      - @messages.each do |m|
        %div{id: "message_#{m.id}"}
          .chatbox
            .chat-face1
              = image_tag "#{m.user.avatar}", class:"avatar"
              .chat-hukidashi
              = m.user.nickname
              %br
              = m.message
              %br
              = m.created_at.strftime("%Y-%m-%d %H:%M")
              = form_with(model: @message, url: edit_message_path(m.id), remote: true, method: :get) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "編集"
              = form_with(model: @message, url: message_path(m.id), remote: true, method: :delete) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "削除"
```
全てのメッセージを表示してます。
form_withの編集と削除の部分だけ解説します！
メッセージを作成したいのでmessages_controllerに情報を送る必要があります。
よって、modelは@messageになります。
editアクションを発火させるパスを記述。
remote: trueでJS形式
モデルとurlを指定してるのでメソッドを記述
`= f.hidden_field :room_id, value: @room.id`で現在表示してるroom_idを送ってあげます。この情報がないと、どのルームでやり取りしてるのかRailsは理解してくれません！

残り3割ぐらいです！

#
```ruby:messages_controller.rb
class MessagesController < ApplicationController
    before_action :set_room, only: [:create, :edit, :update, :destroy]
    before_action :set_message, only: [:edit, :update, :destroy]

    def create
        if Entry.where(user_id: current_user.id, room_id: @room.id)
            @message = Message.create(message_params)
                if @message.save
                    @message = Message.new
                    gets_entries_all_messages
                end
        else
            flash[:alert] = "メッセージの送信に失敗しました"
        end
    end

    def edit
    end

    def update
        if @message.update(message_params)
            gets_entries_all_messages
        end
    end

    def destroy
        if @message.destroy
            gets_entries_all_messages
        end
    end

    private


    def set_room
        @room = Room.find(params[:message][:room_id])
    end

    def set_message
        @message = Message.find(params[:id])
    end

    def gets_entries_all_messages
        @messages = @room.messages.includes(:user).order("created_at asc")
        @entries = @room.entries
    end

    def message_params
        params.require(:message).permit(:user_id, :message, :room_id).merge(user_id: current_user.id)
    end
end

```
先にprivate以下から解説します。
set_roomメソッドは先程form_withで送られたroom_idの情報を取得します。
set_messageメソッドも上と同様です
before_actionでset_roomメソッドとset_messageメソッドを適用してます。

gets_all_messagesメソッドでメッセージを全件取得するのと、
@entriesでルームに参加してる人たちを改めて表示してます。
非同期通信する場合はこの記述が必ず必要です！
毎回データの情報を送ってあげないと、一覧表示できないので。
message_paramsメソッドは新しく送られたメッセージを取得してます。

次はアクション4つを簡単に説明します。

#
```ruby:messages_controller.rb
   def create
        if Entry.where(user_id: current_user.id, room_id: @room.id)
            @message = Message.create(message_params)
                if @message.save
                    @message = Message.new
                    gets_entries_all_messages
                end
        else
            flash[:alert] = "メッセージの送信に失敗しました"
        end
    end

    def edit
    end

    def update
        if @message.update(message_params)
            gets_entries_all_messages
        end
    end

    def destroy
        if @message.destroy
            gets_entries_all_messages
        end
    end
```
createアクションを主に解説します。
createの中のif文はセキュリティのために書いてます。でもぶっちゃけ無くてもいいです笑
@messageをmessage_paramsメソッドを使用してcreateします
メッセージが保存できたら、新しいメッセージを作るための@messageを作成するのと、
gets_entries_all_messagesメソッドでルーム参加者を表示する@entriesと
メッセージ一覧を持ってきます。

updateとdestroyアクションは特に書くこともないので省略します。
コメント投稿機能と処理の流れは同じです。
違いは@entriesを取得する必要があるぐらいですね

後はcreate,edit,update,destroyの各アクションのjs.hamlファイルを作成すれば完成です！
今回はあえて内容は載せません。
ヒントはコメントの非同期投稿機能と全く同じ仕組みです。
自分の[過去の記事][https://qiita.com/kaito_program/items/ef348e170c64d224dd06]
の終盤で詳しく解説してるので、そちらをご参考にして、
ご自身で考えていただければと思います！


ここまでご覧いただきありがとうございます！