2
Help us understand the problem. What are the problem?

posted at

Rails チャット機能

はじめに

前回、Railsアプリケーションにマッチング機能を実装しました。今回は、マッチング成立後のユーザー同士のチャット機能を実装していきたいと思います。

実装環境

Rails7.0.2
Ruby3.1.0

メッセージ機能の仕様

ユーザー同士が、「お気に入り!」評価をしている場合(マッチング成立時)に限り、チャットができるようにします。
ユーザーは複数のチャットに参加することができます。またチャットは、2人でするものとします。

実装の流れ

  1. マッチング成立ユーザーページの実装(viewとcontroller)
  2. チャットルーム用のテーブル(chat_rooms)を作成。
  3. 中間テーブル(chat_room_users)を作成
  4. ルーティングの作成
  5. コントローラーの作成

マッチング成立後、ユーザーページの実装(viewとcontroller)

1.ルーティングの設定

routes.rb
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

スクリーンショット 2022-04-20 23.49.26.png

2.コントローラーの設定

アクションの処理を記述する前に実現したいことをまとめておきます。
マッチング成立条件=
自分がお気に入り評価したユーザーが自分をお気に入り評価している状態。

  1. 自分をお気に入り評価してくれたユーザーを取得。
  2. 1.のうち、自分がお気に入り評価したユーザーを取得(マッチング成立ユーザー)
$ rails g controller matching
matching_controller.rb
 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メソッドを使用して、引数の條件に一致するデータを取得しています。
具体的には、

  1. ログインユーザーに「like」と評価したユーザーに関するデータをLikeモデルから配列で取得。
  2. 取得した配列のうち、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

ログインユーザーとマッチングが成功したユーザーの情報を取得しています。
具体的には、

  1. ログインユーザーへ「like」評価をしたユーザーのうち、ログインユーザーからも「like」評価をしている情報をLikeモデルから配列で取得。
  2. mapメソッドにより、取得した配列から要素を取り出し、ブロック処理を実効。実効した結果を要素に持つ新しい配列を作成。

参考:4.1.1 belongs_toで追加されるメソッド
参考:pluck
参考:where
参考:【Ruby】 mapメソッドの基礎から応用の使い方まとめ

3.Viewの設定

matching/index.html.erb
<%= render "partial/navbar" %>
<p>マッチング中のお相手 <%= @matching_users.size %></p>
<% @mathing_users.each do |matching_user| %>
    <ul>
        <li>
            <%= link_to matching_user.username,"#" %>
        </li>
    </ul>
<% end %>

リンクについては、この後設定しますので、一旦ダミーリンクを貼っておきます。
スクリーンショット 2022-04-23 12.56.19.png

マッチング成立ユーザーの取得に成功しています。
このように表示されていれば、大丈夫です。

チャットルーム用のテーブル(chat_rooms)を作成。

マイグレーションファイルの作成

$ rails g model ChatRooms

マイグレーションファイルの確認

2022xxxxxx_create_chat_rooms.rb
class CreateChatRooms < ActiveRecord::Migration[7.0]
  def change
    create_table :chat_rooms do |t|

      t.timestamps
    end
  end
end

データベースをマイグレート

$ rails db:migrate

データベース構造の確認

schema.rb
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人)を持ちます。
つまり、二つのモデルは、多対多の関係性にあることがわかります。

スクリーンショット 2022-04-20 19.37.34.png

多対多の関係性である場合は、中間テーブルを用意して、その関係性を表します。

◯多対多の場合に、中間テーブルが必要な理由

中間テーブルを使わないと、、、、、

  • 外部キーを記録するカラム数が多くなる。(1カラムにつき、1データのため)
  • 使われないカラムが出てくる。(null値が入るカラムができる。)

上記のような、DB設計のアンチパターンになってしまいます。

参考:【Rails】 アソシエーションを図解形式で徹底的に理解しよう!

中間テーブル(chat_room_users)を作成

マイグレーションファイルの作成

$ rails g model ChatRoomUsers 

マイグレーションファイルを編集

2022xxxxxxxxx_create_chat_room_users
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

データベースの構造を確認

schema.rb
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図

スクリーンショット 2022-04-20 20.09.19.png

中間テーブルを作成し、外部キー情報を持つことにより、
usersとchat_roomsテーブルに外部キーを記録するための不要なカラムが追加されずに済みます。

モデルの設定

user.rb
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
chat_room.rb
class ChatRoom < ApplicationRecord
    has_many :chat_room_users
    has_many :users, through: :chat_room_users 
end
chat_room_user.rb
class ChatRoomUser < ApplicationRecord
    belongs_to :chat_room
    belongs_to :user
end

modelでhas_many,belongs_toの設定を行うことで、関連するオブジェクトを取得することのできる、associationメソッドを使用できるようになります。

参考:4.1 belongs_to関連付けの詳細

ルーティングの作成

routes.rb
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アクションから実装していきます。

chat_rooms_controller.rb
 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の作成

chat_rooms/show.html.erb
<p>Chatroomへようこそ!</p>

go_to_chatroom.gif

Chatroomページヘ遷移すれば成功です。

メッセージ機能の実装

いよいよ本題のメッセージ機能の実装を行います。

1.chat_messagesテーブルの作成

$ rails g model ChatMessages
2022xxxxxxx_create_chat_messages.rb
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
schema.rb
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対多
chat_message.rb
class ChatMessage < ApplicationRecord
    belongs_to :chat_room
    belongs_to :user
end
chat_room.rb
class ChatRoom < ApplicationRecord
    has_many :chat_room_users
    has_many :users,through: :chat_room_users
    #ここから
    has_many :chat_messages
    #ここまで追記
end
user.rb
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対多

スクリーンショット 2022-04-23 14.40.50.png

4.ルーティングの設定

rutes.rb
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アクションを実装していきます。
ここでチャットルームのメッセージを表示したいと思います。
メッセージを表示する際は、「誰が」「いつ」送信したメッセージなのかも併せて表示できるようにします。

chat_rooms_controller.rb
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アクションを定義します。

chat_messages_controller.rb
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の編集

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 モデルオブジェクトを扱う

メッセージ.gif
チャットルーム内のフォームでメッセージの送信。表示ができていれば、成功です。

参考

Railsガイド
【Rails】 アソシエーションを図解形式で徹底的に理解しよう!
4.1.1 belongs_toで追加されるメソッド
pluck
where
【Ruby】 mapメソッドの基礎から応用の使い方まとめ

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?