LoginSignup
10
19

More than 3 years have passed since last update.

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

Posted at

なにこれ

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

前提条件

・Usersテーブルが作成済みであること


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

テーブル設計

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

Usersテーブル

今回の場合はnicknameカラムとavatarカラムを追加してますが、
ここは個人で編集して下さい。
avatarは必要なさそうならコメントアウトすることをオススメします。

マイグレーションファイル
  def change
    add_column :users, :nickname, :string
    add_column :users, :avatar, :string
  end

Entriesテーブル

どのユーザーがどのルームに入ったかを管理するのがEntriesテーブルの役割です!
user_idとroom_idに外部キー制約をかけます。

マイグレーションファイル
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テーブルの役割です。
このテーブルにカラムは必要ありません!

マイグレーションファイル
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に外部キー制約をかけます。

マイグレーションファイル
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の情報をかき集めてきます!

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]

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モデルにもあった、一覧表示をするのに必要です。

room.rb
    has_many :messages
    has_many :entries
    has_many :users, through: :entries

messageモデル

message.rb
    belongs_to :user
    belongs_to :room

    validates :message, presence: true


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

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

usersコントローラーの編集

以下の記述になります。上から順番に区切って説明します。

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
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で一致するものがあるのか判断します。

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

Entriesテーブル.png

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

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

users_controller.rb
        unless @haveRoom
          @room = Room.new
          @entry = Entry.new
        end

@habeRoomに値がなかったら、新規作成するための変数を作成します。
form_forで@room@entryのの情報を送るためです。

ビューの編集

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

/users/show.html.haml
= render "contact"

DMルームに飛ぶための記述です。
こちらも区切って解説します!

/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 "ダイレクトメッセージ"
/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行目にルームに参加するためのリンクを表示してます!

/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テーブルが1対多の関係の時に
entryテーブルに値を保存できるようにするために使用した。

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

roomsコントローラーの編集

全体の記述は以下のようになります。上から順番に区切って解説します。

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ルームの一覧表示をするための記述です。

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にリダイレクトさせます。

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テーブルでヒットした情報、またはエントリーしてる)ユーザーを取得

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

/rooms/show/1/.html.haml
.div#chat_area
  = render "chat_area"
.div#chat_form
  = render "chat_form"

チャット全体表示と投稿フォームの部分テンプレートを表示してます!

/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 "削除"

上から区切って説明します!

/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に代入したユーザー情報を表示させてます。

/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割ぐらいです!

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つを簡単に説明します。

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]
の終盤で詳しく解説してるので、そちらをご参考にして、
ご自身で考えていただければと思います!

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

10
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
19