はじめに
この記事はプログラミング初学者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。内容に不備などあればご指摘いただますと幸いです。
今回Xクローン作成中DM機能を実装しました。
そのDM機能について備忘録も兼ねて記事を作成していきます。
実現したいこと
ユーザー同士、1対1でメッセージのやり取りできる機能を実装すること。
DM機能の流れ
相手ユーザーのツイートorサイドバーのメニューからメッセージ画面に遷移する。
↓
roomがなければ作成する。
↓
メッセージ画面でメッセージを送信する。
このように一般的なDM機能を作成していきます。
DM機能について
テーブル設計は以下のようになります。

まず、usersテーブルはユーザーを管理するテーブルです。
roomsテーブルはチャットルーム(メッセージをやり取りする場所)となります。
ユーザーは複数のroomを持つ可能性があります。また、roomは複数(2人)のユーザーを持ちます。これは多対多の関係になるので、中間テーブルとしてentriesテーブルを置き情報を管理します。
ユーザーは複数のメッセージを送ることができます。また、roomでは複数のメッセージを受け取ることになります。これも多対多の関係になるので、中間テーブルとしてmessagesテーブルを置き情報を管理します。
モデルとテーブルの作成
それでは、実装していきましょう!
以下のコマンドでモデルを作成します。
$ rails g model room
$ rails g model entry user_id:integer room_id:integer
$ rails g model message user_id:integer room_id:integer content:text
生成されたentry
モデルのマイグレーションファイルを以下のように編集します。
class CreateEntries < ActiveRecord::Migration[7.0]
def change
create_table :entries do |t|
t.integer :user_id
t.integer :room_id
t.timestamps
end
add_index :entries, %i[user_id room_id], unique: true # 追加
end
end
同じルームに同じユーザーが入ることを避けたいので、add_index
を使って一意制約を記述します。
これで、user_idとroom_idの組み合わせをユニーク(1通り)に設定します。
以下のコマンドでマイグレーションを実行しましょう。
$ rails db:migrate
アソシエーションの設定
それぞれのファイルに以下を追加します。
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
belongs_to :user
belongs_to :room
# 同じルームに同じユーザーが入ることを避けたいので、一意制約を記述します。
# これで、user_idとroom_idの組み合わせをユニーク(1通り)に設定します。
validates :user_id, uniqueness: { scope: :room_id }
DB側と同じようにアプリケーション側にも一意制約を記述しています。
belongs_to :user
belongs_to :room
validates :content, presence: true
ルーティングの設定
以下のように記述します。
resources :users do
resources :rooms, only: [:create]
end
resources :rooms, only: [:index] do
resources :messages, only: [:create]
end
ネストすることで、users/:user_id/rooms
とrooms/:room_id/messages
のようなURLができます。
こうすることで、createアクションでuser_id
はparams[:user_id]
として、room_id
はparams[:room_id]
として取得できます。
Controllerの作成
以下のコマンドでコントローラを作成します。
$ rails g controller rooms
$ rails g controller messages
生成されたコントローラのファイルを以下のように編集します。
class RoomsController < ApplicationController
def create
@user = User.find_by(id: params[:user_id]) # チャットする相手を取得
my_room_ids = current_user.entries.pluck(:room_id) # ログイン中のユーザーの全てのチャットルームIDを取得
existing_room = @user.entries.find_by(room_id: my_room_ids) # チャットする相手とのルームがあるか確認
if existing_room.nil # ルームがない場合
@room = Room.create # 新しくルームを作成
@room.entries.create(user_id: current_user.id) # 自分の中間テーブルを作成
@room.entries.create(user_id: @user.id) # 相手の中間テーブルを作成
end
redirect_to rooms_path
end
def index
my_room_ids = current_user.entries.pluck(:room_id) # ログイン中のユーザーの全てのチャットルームIDを取得
# 相手のエントリー情報を取得
@another_entries = Entry.where(room_id: my_room_ids)
.where.not(user_id: current_user.id)
.includes(user: { icon_image_attachment: :blob })
if params[:tab] == 'room' # 右側のチャットルームでメッセージを表示できるようにする
@room = Room.find(params[:room_id]) # ルームを取得する
@messages = @room.messages # ルームに関連したメッセージを取得
@message = Message.new # form_withに渡すため空のインスタンスを生成
# 相手のエントリー情報を取得
@another_entry = @room.entries.where.not(user_id: current_user.id).first
end
end
end
create
アクションでは、ログイン中のユーザーと相手ユーザーのroomが存在するか調べ、なければ作成します。
index
アクションのif params[:tab] == 'room'
は、link_to
のクエリパラメータで渡された値を使っています。
class MessagesController < ApplicationController
def create
@room = Room.find(params[:room_id])
@message = @room.messages.build(user_id: current_user.id, content: params[:message][:content])
@message.save!
redirect_to request.referer, notice: 'メッセージが送信されました'
end
end
Viewの作成
チャットルーム一覧の表示、メッセージ一覧の表示、メッセージ投稿フォームの設置をしていきます。
div.message-contents
= render 'layouts/sidebar'
main.main.border-start.border-end
div.bar.d-flex.align-items-center
div.bar-name
| メッセージ
# チャットルーム一覧の表示
div
- @another_entries.each do |entry|
= link_to rooms_path(tab: 'room', room_id: "#{entry.room.id}"), class: "room-link" do # クエリパラメータを渡す
div.d-flex class="#{'active-room' if params[:room_id] == "#{entry.room.id}"} one-room"
div
= image_tag entry.user.icon_image.variant(resize_to_fill: [40, 40]).processed, class: "post-user-img rounded-circle"
div
div
span.room-name
= entry.user.name
span.room-date
= entry.room.messages.last&.created_at&.to_fs(:room_datetime_jp) #「ぼっち演算子 &.」レシーバがnilであった場合でもエラーが発生しない
div.room-content
span
= entry.room.messages.last&.content
div.message-container.border-end
-if params[:tab] == 'room' # チャットルーム一覧をクリックでメッセージ一覧が表示される
div.bar.d-flex.align-items-center.border-bottom
div
= image_tag @another_entry.user.icon_image.variant(resize_to_fill: [35, 35]).processed, class: "post-user-img rounded-circle"
div.bar-room-name
= @another_entry.user.name
# メッセージ一覧の表示
div.exchanging-messages-container.pt-4
div.exchanging-messages
- @messages.each do |message|
- if message.user == current_user # 自分のメッセージを表示
div.d-flex.flex-column.align-items-end.mb-3
div.my-message
= simple_format(message.content, class: "simple_format")
div.message-date
= message.created_at.to_fs(:room_datetime_jp)
- else # 相手のメッセージを表示
div.d-flex.flex-column.align-items-start.mb-3
div.another-message
= message.content
div.message-date
= message.created_at.to_fs(:room_datetime_jp)
# メッセージ投稿フォームの設置
div.message-form-container
= form_with model: [@room, @message], url: room_messages_path(@room) do |f|
div.d-flex.align-items-center.justify-content-center
= f.text_area :content, class: "message-form", placeholder: "新しいメッセージを作成"
= button_tag type: "submit", class: "form-btn" do
i.bi.bi-send
メッセージ一覧の部分をif文を用いて分岐させることによって、DMっぽく自分のメッセージは右側、相手のメッセージは左側のようにスタイリングすることができます。
form_withを記述する際、ルーティングでネストを定義している時は[@room, @message]
のように、配列で二つ ([関連元のインスタンス, 関連先のインスタンス]) 渡す必要があります。
おわりに
最後まで読んでいただきありがとうございました。
少しでも皆さんの参考になれば幸いです。
参考にしたサイト