はじめに
前回、Railsアプリケーションにマッチング機能を実装しました。今回は、マッチング成立後のユーザー同士のチャット機能を実装していきたいと思います。
実装環境
Rails7.0.2
Ruby3.1.0
メッセージ機能の仕様
ユーザー同士が、「お気に入り!」評価をしている場合(マッチング成立時)に限り、チャットができるようにします。
ユーザーは複数のチャットに参加することができます。またチャットは、2人でするものとします。
実装の流れ
- マッチング成立ユーザーページの実装(viewとcontroller)
- チャットルーム用のテーブル(chat_rooms)を作成。
- 中間テーブル(chat_room_users)を作成
- ルーティングの作成
- コントローラーの作成
マッチング成立後、ユーザーページの実装(viewとcontroller)
1.ルーティングの設定
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
2.コントローラーの設定
アクションの処理を記述する前に実現したいことをまとめておきます。
マッチング成立条件=
自分がお気に入り評価したユーザーが自分をお気に入り評価している状態。
- 自分をお気に入り評価してくれたユーザーを取得。
- 1.のうち、自分がお気に入り評価したユーザーを取得(マッチング成立ユーザー)
$ rails g controller matching
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が「1」のユーザーがidが「2」、「3」の2人のユーザーから「お気に入り!」の評価を受けている場合の戻り値は以下のようにな配列となります。
#=>[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
各処理について確認します。
got_like_users_ids = Like.where(to_user_id: current_user.id, status: "like").pluck(:from_user_id)
Likeモデルからwhereメソッドを使用して、引数の條件に一致するデータを取得しています。
具体的には、
- ログインユーザーに「like」と評価したユーザーに関するデータをLikeモデルから配列で取得。
- 取得した配列のうち、procメソッドで「from_user_id」のカラム情報のみを取得。
@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
ログインユーザーとマッチングが成功したユーザーの情報を取得しています。
具体的には、
- ログインユーザーへ「like」評価をしたユーザーのうち、ログインユーザーからも「like」評価をしている情報をLikeモデルから配列で取得。
- mapメソッドにより、取得した配列から要素を取り出し、ブロック処理を実効。実効した結果を要素に持つ新しい配列を作成。
参考:4.1.1 belongs_toで追加されるメソッド
参考:pluck
参考:where
参考:【Ruby】 mapメソッドの基礎から応用の使い方まとめ
3.Viewの設定
<%= render "partial/navbar" %>
<p>マッチング中のお相手 <%= @matching_users.size %>人</p>
<% @mathing_users.each do |matching_user| %>
<ul>
<li>
<%= link_to matching_user.username,"#" %>
</li>
</ul>
<% end %>
リンクについては、この後設定しますので、一旦ダミーリンクを貼っておきます。
マッチング成立ユーザーの取得に成功しています。
このように表示されていれば、大丈夫です。
チャットルーム用のテーブル(chat_rooms)を作成。
マイグレーションファイルの作成
$ rails g model ChatRooms
マイグレーションファイルの確認
class CreateChatRooms < ActiveRecord::Migration[7.0]
def change
create_table :chat_rooms do |t|
t.timestamps
end
end
end
データベースをマイグレート
$ rails db:migrate
データベース構造の確認
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」テーブルの関連性については、次の通りです。
一人のユーザーは、複数のチャットルームを持ち、
一つのチャットルームでは、複数人のユーザー(2人)を持ちます。
つまり、二つのモデルは、多対多の関係性にあることがわかります。
多対多の関係性である場合は、中間テーブルを用意して、その関係性を表します。
◯多対多の場合に、中間テーブルが必要な理由
中間テーブルを使わないと、、、、、
- 外部キーを記録するカラム数が多くなる。(1カラムにつき、1データのため)
- 使われないカラムが出てくる。(null値が入るカラムができる。)
上記のような、DB設計のアンチパターンになってしまいます。
参考:【Rails】 アソシエーションを図解形式で徹底的に理解しよう!
中間テーブル(chat_room_users)を作成
マイグレーションファイルの作成
$ rails g model ChatRoomUsers
マイグレーションファイルを編集
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
データベースをマイグレート
$ rails db:migrate
データベースの構造を確認
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図
中間テーブルを作成し、外部キー情報を持つことにより、
usersとchat_roomsテーブルに外部キーを記録するための不要なカラムが追加されずに済みます。
モデルの設定
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
class ChatRoom < ApplicationRecord
has_many :chat_room_users
has_many :users, through: :chat_room_users
end
class ChatRoomUser < ApplicationRecord
belongs_to :chat_room
belongs_to :user
end
modelでhas_many,belongs_toの設定を行うことで、関連するオブジェクトを取得することのできる、associationメソッドを使用できるようになります。
ルーティングの作成
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コントローラー)
$ rails g controller chat_rooms
まずは、createアクションから実装していきます。
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の作成
<p>Chatroomへようこそ!</p>
Chatroomページヘ遷移すれば成功です。
メッセージ機能の実装
いよいよ本題のメッセージ機能の実装を行います。
1.chat_messagesテーブルの作成
$ rails g model ChatMessages
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
$ rails db:migrate
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は1対多
- UserとChatMessageは1対多
class ChatMessage < ApplicationRecord
belongs_to :chat_room
belongs_to :user
end
class ChatRoom < ApplicationRecord
has_many :chat_room_users
has_many :users,through: :chat_room_users
#ここから
has_many :chat_messages
#ここまで追記
end
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は1対多
- UserとChatMessageは1対多
4.ルーティングの設定
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アクションを実装していきます。
ここでチャットルームのメッセージを表示したいと思います。
メッセージを表示する際は、「誰が」「いつ」送信したメッセージなのかも併せて表示できるようにします。
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)
$ rails g controller chat_messages
メッセージを作成するためのcreateアクションを定義します。
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の編集
<%= 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 モデルオブジェクトを扱う
チャットルーム内のフォームでメッセージの送信。表示ができていれば、成功です。
参考
Railsガイド
【Rails】 アソシエーションを図解形式で徹底的に理解しよう!
4.1.1 belongs_toで追加されるメソッド
pluck
where
【Ruby】 mapメソッドの基礎から応用の使い方まとめ