#なにこれ
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の情報をかき集めてきます!
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]
belongs_to :user
belongs_to :room
validates :room_id, uniqueness: { scope: :user_id }
#roomモデル
Roomsは複数のメッセージとエントリー情報を持つのでhas_manyになります!
has_many :users, through: :entries
はuserモデルにもあった、一覧表示をするのに必要です。
has_many :messages
has_many :entries
has_many :users, through: :entries
#messageモデル
belongs_to :user
belongs_to :room
validates :message, presence: true
#ルーティングの設定
全体の流れ
1,ユーザーの詳細ページからDMルームに飛ぶ
2,ルームは参加と一覧表示ができる
3,メッセージは投稿、編集、削除ができる
resources :users, only: [:show]
resources :rooms, only: [:index, :create:, :show]
resources :messages, only: [:create, :edit, :update, :destroy]
#usersコントローラーの編集
以下の記述になります。上から順番に区切って説明します。
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
@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で一致するものがあるのか判断します。
画像の例を出しておきます!
このテーブルの中に一致する内容があったらそこで処理が終わります。
既にルームが作成されてるので。
もし作成されてないなら、新規にルームを作成する処理に分岐します!
もし部屋が作成されてなかったら、以下の処理をします。
@haveRoom = true
を代入
@roomId = cu.room_id
を代入します。
@haveRoomは既にルームがあるよ。という意味の変数です。
@roomIdは後ほど記述しますが、ルームにアクセスするための変数です。
unless @haveRoom
@room = Room.new
@entry = Entry.new
end
@habeRoomに値がなかったら、新規作成するための変数を作成します。
form_forで@roomと@entryのの情報を送るためです。
#ビューの編集
次はビューの編集をします。ここから必要な部分の一部抜粋になります
ユーザー詳細ページからDMルームに飛ぶための部分テンプレートを用意してrenderします
= render "contact"
DMルームに飛ぶための記述です。
こちらも区切って解説します!
- 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 "ダイレクトメッセージ"
- 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行目にルームに参加するためのリンクを表示してます!
- 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コントローラーの編集
全体の記述は以下のようになります。上から順番に区切って解説します。
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ルームの一覧表示をするための記述です。
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にリダイレクトさせます。
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テーブルでヒットした情報、またはエントリーしてる)ユーザーを取得
次はルームのビューを作成します!
.div#chat_area
= render "chat_area"
.div#chat_form
= render "chat_form"
チャット全体表示と投稿フォームの部分テンプレートを表示してます!
.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 "削除"
上から区切って説明します!
.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に代入したユーザー情報を表示させてます。
.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割ぐらいです!
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つを簡単に説明します。
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]
の終盤で詳しく解説してるので、そちらをご参考にして、
ご自身で考えていただければと思います!
ここまでご覧いただきありがとうございます!