🔴

railsでDM機能を作成する

2023/02/26に公開

記事作成日:2020年05月17日

##完成目標##
ユーザー2人が会話できるチャットルーム(DM)を作成する

実装する

解説は後ほど行うので、コードをコピペしてまずは作ってしまいましょう!
まずは適当なプロジェクトを作成する

$ rails new dm_app

次に各テーブルを作成していく。
DM機能を実装するためには以下のモデルが必要になる。
Userモデル   → ユーザーを管理する
Entrieモデル  → どのユーザーがどのルームに属しているかを管理する
Roomモデル  → チャットルームに2人のユーザーが入っているかを管理する
Messageモデル → ユーザーがルームに送信するメッセージを管理する

まずはdeviseを用いてUserモデルを作成する
Gemfileに以下を追加し、bundle installしてdeviseを導入する。

gem 'devise'
$ rails g devise:install

deviseに対応したコントローラの作成
$ rails g devise:controllers users

Usersモデルを作成
$ rails g devise user 

deviseに対応したビューの作成
$ rails g devise:views

$ rails db:create
$ rails db:migrate

コントローラーを作成する

$ rails g controller users index show
$ rails g controller rooms show
$ rails g controller messages

モデルを作成する
Userはdeviseで作成したので残りのRoom, Entry, Messageを作成する

$ rails g model room 
$ rails g model entry user:references room:references
$ rails g model message user:references room:references body:text

$ rails db:migrate

アソシエーション(関連付け)

models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
end
models/entry.rb
class Room < ApplicationRecord
  belongs_to :user
  belongs_to :room
end
models/room.rb
class Room < ApplicationRecord
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
end
models/message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

コントローラーを編集する

users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!
  def index
    @users=User.all
  end

  def show
    @user=User.find(params[:id])
    @currentUserEntry=Entry.where(user_id: current_user.id)
    @userEntry=Entry.where(user_id: @user.id)
    if @user.id == current_user.id
      @msg ="他のユーザーとDMしてみよう!"
    else
      @currentUserEntry.each do |cu|
        @userEntry.each do |u|
          if cu.room_id == u.room_id then
            @isRoom = true
            @roomId = cu.room_id
          end
        end
      end

      if @isRoom != true
        @room = Room.new
        @entry = Entry.new
      end

    end
  end
end
rooms_controller.rb
class RoomsController < ApplicationController
    before_action :authenticate_user!
    def create
      @room = Room.create
      Entry.create(room_id: @room.id, user_id: current_user.id)
      Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
      redirect_to "/rooms/#{@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.all
        @message = Message.new
        @entries = @room.entries
      else
        redirect_back(fallback_location: root_path)
      end
    end
  end
messages_controller.rb
class MessagesController < ApplicationController
    before_action :authenticate_user!

    def create
      if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
        @message = Message.new(message_params)
        if @message.save
            redirect_to "/rooms/#{@message.room_id}"
        end
      else
        redirect_back(fallback_location: root_path)
      end
    end

    private 
    def message_params
        params.require(:message).permit(:user_id, :body, :room_id).merge(user_id: current_user.id)
    end
  end

ルーティングを作成する

routes.rb
Rails.application.routes.draw do

  devise_for :users
  root "users#index"
  resources :users, :only => [:index, :show]
  resources :messages, :only => [:create]
  resources :rooms, :only => [:create, :show]
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

ビューを作成する


<h1>DMサンプル</h1>
<h2>User一覧ページ</h2>

<% if user_signed_in? %>
    <h2><%= current_user.email %>でログインしています</h2> 
    <%= link_to "SignOut", destroy_user_session_path, :method => :delete, class:"signOutButton" %>


    <% number = 1%>
    <div class="flex">
        <% @users.each do |u| %>
        <div class="user-in">
            <p>プロフィール No.<%= number %></p>
            <p><%= u.email %>さん</p>
            <p><%= link_to 'ユーザーページへ', user_path(u.id) %></p>
        </div>
        <% number += 1 %>
        <% end %>
    </div>

<% else %>
  <%= link_to "SignUp", new_user_registration_path %>
  <br>
  <%= link_to "SignIn", new_user_session_path %>
<% end %>

<style>
.signOutButton{
    text-decoration:none;
    display:inline-block;
    border:3px solid royalblue;
    background-color:aliceblue;
    padding:5px;
}
.flex{
    display:flex;
    flex-wrap:wrap;    
}
.user-in{
    display:block;
    width:300px;
    text-align:center;
    border:1px solid darkgray;
    background-color:gainsboro;
    flex-shrink: 0;
}
</style>
<h1>ユーザー詳細ページ</h1>

<div class="user-in">
<h2><%= @user.email %></h2>

<% if @user.id == current_user.id %>
    <%= @msg %>
<% else %>
    <% if @isRoom == true %>
        <p><%= link_to 'DMへ', room_path(@roomId) %></p>
    <% else %>
        <%= form_for @room do |f| %>
            <%= fields_for @entry do |e|%>
                <% e.hidden_field :user_id, value: @user.id %>
            <% end %>
        <%= f.submit "DMを開始する"%>
        <% end %>
    <% end %>
<% end %>
</div>


<%= link_to "ユーザー一覧に戻る", users_path %>



<style>
.user-in{
    width:300px;
    padding:10px;
    margin:10px;
    text-align:center;
    border:1px solid darkgray;
    background-color:gainsboro;
}
</style>

<div class="user-in">
<% @entries.each do |e| %>
    <h3><strong><a href="/users/<%= e.user.id %>"><%= e.user.email%></a></strong></h3>
<% end %>
<p>2人のDM❤️</p>
</div>

<div class="dmMain">
    <% @messages.each do |m| %>
        <strong><%= m.body %></strong>
        <small><%= m.user.email %>さん</small><hr>
    <% end %>

    <%= form_for @message do |f| %>
    <%= f.text_field :body, :placeholder => "メッセージを入力して下さい" , :size => 100 %>
    <%= f.hidden_field :room_id, :value => @room.id %>
    <br>
    <%= f.submit "送信する" %>
    <% end %>
</div>

<%= link_to "ユーザー一覧に戻る", users_path %>


<style>
.user-in{
    text-align:center;
    border:1px solid darkgray;
    background-color:gainsboro;
}
.dmMain{
    padding:30px;
    border:1px solid darkgray;
    background-color:gainsboro;
}
</style>

解説

テーブル設計を考える(多対多のアソシエーション)

DM機能を作るためにどんなテーブルが必要でしょうか?
UsersテーブルとRoomsテーブルを用意すればDM機能が完成するのでは:thinking:と考えた人は間違いです。

ユーザーは他のユーザーとDMをするためにルームを作ります。下記の図(汚くてすみません汗)ではuser.id:1がuser.id:2とuser.id:3とuser.id:4と会話するために3つのルームを作っています。つまり1人のユーザーが複数のルームを抱えています。対してルームはというとRoom.id:1にはUser.id:1とUser.id:2の2人のユーザーが入っています。つまりルームも複数(2人)のユーザーを抱えているわけです。この関係を多対多(M:N)の関係と呼びます。

多対多の関係を実現するには中間テーブルを利用します。

今回はUsersテーブルとRoomsテーブルが多対多の関係になっているので中間テーブルEntriesを置き、どのユーザーがどのルームに所属しているかの情報を管理します。またRoomsテーブルでも複数(2人)のユーザーが複数のメッセージを送る多対多の関係であるため同じように中間テーブルMessagesを置き、どのユーザーがどのルームでどんなメッセージを送ったのかを管理します。

上記の表はそれぞれの関係性を図にしたものである。
1人のUserが複数のEntryを持っており、
Entryは誰か1人のUserに所属している。

Roomは複数(2人だけのルームなので2つ)のEntryを持っており、
Entryはどこか1つのRoomに所属している。

1人のUserは複数のMessageをやり取りするので、
Messageは送信主の1人のUserに所属する。

Roomはそこでやりとりをする複数のMessageを持っており、
Messageは特定(1つだけ)のRoomに所属する


モデルを作る際は下記のようになる。(Userはdeviseで!)

$ rails g model room 
$ rails g model entry user:references room:references
$ rails g model message user:references room:references body:text

references型は外部キーを保存する時に使用します。
今回はentrysとmessagesが中間テーブルになるのでuser_idとroom_idを外部キーとして持つ必要があります。そのため、user:referencesとroom:referencesと入力します。こうすることでmigrateファイルでは下記のように記載してくれます。

migrate/_create_entries.rb
class CreateEntries < ActiveRecord::Migration[6.0]
  def change
    create_table :entries do |t|
      t.references :user, null: false, foreign_key: true
      t.references :room, null: false, foreign_key: true

      t.timestamps
    end
  end
end
migrate/_create_messages.rb
class CreateMessages < ActiveRecord::Migration[6.0]
  def change
    create_table :messages do |t|
      t.references :user, null: false, foreign_key: true
      t.references :room, null: false, foreign_key: true
      t.text :body

      t.timestamps
    end
  end
end

アソシエーションを記述する

先ほど図に落としこんだテーブルの関係性を実際にmodelsファイルにアソシエーションの記述をすると下記のようになる。

models/user.rb
class User < ApplicationRecord
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
end
models/entry.rb
class Room < ApplicationRecord
  belongs_to :user
  belongs_to :room
end
models/room.rb
class Room < ApplicationRecord
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
end
models/message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

belongs_to (モデル名)の後にdependent: :destroyをつけることで親元のデータが消去された時に子のデータも自動で消去されるようになるので付けときましょう!

コントローラーを作成してルーティングを設定する

コントローラーを作成する
①users/indexページでDMをしたいユーザーを探す(ユーザー一覧ページ) →
②users/showページでDMを開始するボタンを押す →
③room/showページでログインしているユーザーとDMボタンを押されたユーザーが会話する(messageのbodyがcreateされる)
このような流れで作りたいので下記のようにコントローラーとページを作成する。

$ rails g controller users index show
$ rails g controller rooms show
$ rails g controller messages

ルーティングを作成する

routes.rb
Rails.application.routes.draw do
  devise_for :users
  root "users#index"
  resources :users, :only => [:index, :show]
  resources :messages, :only => [:create]
  resources :rooms, :only => [:create, :show]
end

resourcesについては https://qiita.com/GeekSalon/private/57a6be49186ef3bce9f3

Usersコントローラー

users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!
  def index
    @users=User.all
  end

  def show
    @user=User.find(params[:id])
    @currentUserEntry=Entry.where(user_id: current_user.id)
    @userEntry=Entry.where(user_id: @user.id)
    if @user.id == current_user.id
      @msg ="他のユーザーとDMしてみよう!"
    else
      @currentUserEntry.each do |cu|
        @userEntry.each do |u|
          if cu.room_id == u.room_id then
            @isRoom = true
            @roomId = cu.room_id
          end
        end
      end

      if @isRoom != true
        @room = Room.new
        @entry = Entry.new
      end

    end
  end
end
    @user=User.find(params[:id])
    @currentUserEntry=Entry.where(user_id: current_user.id)
    @userEntry=Entry.where(user_id: @user.id)

上から順番に読み解いていきましょう
まずはユーザーの詳細ページなのでfindメソッドでユーザーの情報を所得しています。
2行目と3行目ではwhereメソッドを使って、中間テーブルEntrysテーブルの中に記録されているcurrent_user(2行目)とfindメソッドで所得した詳細ページのユーザーのuser_idが含まれているレコードを全て所得して、それぞれインスタンス変数currentUserEntryとuserEntryに代入しておきます。
*モデル名.where(カラム名: 条件) とすることで条件に合うレコードを全て取得することができます。

次にif文で2パターンの条件分岐をさせています。1つ目はcurrent_userが自分の詳細ページにアクセスした場合(例外)、2つ目がメインとなるcurrent_userが自分以外のユーザーの詳細ページにアクセスした場合です。自分で自分のユーザー詳細ページにアクセスしたユーザーには「他のユーザーとDMしてみよう」というメッセージを渡してあげましょう🍀

      @currentUserEntry.each do |cu|
        @userEntry.each do |u|
          if cu.room_id == u.room_id then
            @isRoom = true
            @roomId = cu.room_id
          end
        end
      end

ここで先ほど用意した変数の@currentUserEntryと@userEntryを使います。2つの変数の中に代入されているEntryテーブルの中身をeach文を用いて1つずつ取り出し、同じroom_idが発見された場合はcurrentUserEntryとuserEntryの2人が既にDMのルームを作っているということになるので、
@isRoom = true
@roomId = cu.room_id
を実行します。@isRoom = true と記述することでtrueではないとき、すなわちDMのルームが未作成の場合の処理を後に実行するので必要になります。@roomID = cu.room_id ではshowページで <%= link_to 'DMへ', room_path(@roomId) %> このようにDMのルームに行くリンクのパスを設定するために記述しています。そのためcu.room_idではなくu.room_idでも問題ありません👍

      if @isRoom != true
        @room = Room.new
        @entry = Entry.new
      end

そしてここでは2人のルームが未作成だった場合の処理を実行しています。

Roomsコントローラー

rooms_controller.rb
class RoomsController < ApplicationController
    before_action :authenticate_user!
    def create
      @room = Room.create
      Entry.create(room_id: @room.id, user_id: current_user.id)
      Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
      redirect_to "/rooms/#{@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.all
        @message = Message.new
        @entries = @room.entries
      else
        redirect_back(fallback_location: root_path)
      end
    end
  end
    def create
      @room = Room.create
      Entry.create(room_id: @room.id, user_id: current_user.id)
      Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
      redirect_to "/rooms/#{@room.id}"
    end

ユーザーの詳細画面の<%= form_for @room do |f| %>で送られてきた情報をここでcreateします。
ここではRoomを新しく作る以外にも、Entryにどのユーザーがどのルームに属しているかを記録する必要があります。すなわちuser_idとroom_idをEntryに記録する必要があるのです。当然current_userとcurrent_userによってDMを開始するを押された詳細ページのユーザーの2人のuser_idとroom_idを記録しておくことになります。そのためcreateメソッドが2回登場しています。
1つ目はcurrent_userのuser_idとroom_idを記録しています。
2つ目のcreateで詳細ページのユーザーのuser_idとroom_idを記録しています。room_idは同じルームなので共通であるため@room.idを記録しています。user_idは<% e.hidden_field :user_id, value: @user.id %>から所得して記録しています。@userはコントローラーで@user=User.find(params[:id])このように定義されているので詳細ページのユーザーのuser_idがEntryに記録される(作られる)ことになります。
そして処理が終わるとredirectされます。

    def show
      @room = Room.find(params[:id])
      if Entry.where(user_id: current_user.id, room_id: @room.id).present?
        @messages = @room.messages.all
        @message = Message.new
        @entries = @room.entries
      else
        redirect_back(fallback_location: root_path)
      end
    end
messages_controller.rb
class MessagesController < ApplicationController
    before_action :authenticate_user!

    def create
      if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
        @message = Message.new(message_params)
        if @message.save
            redirect_to "/rooms/#{@message.room_id}"
        end
      else
        redirect_back(fallback_location: root_path)
      end
    end

    private 
    def message_params
        params.require(:message).permit(:user_id, :body, :room_id).merge(user_id: current_user.id)
    end
  end

Discussion