railsでDM機能を作成する
記事作成日: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
アソシエーション(関連付け)
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
class Room < ApplicationRecord
belongs_to :user
belongs_to :room
end
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
end
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
end
コントローラーを編集する
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
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
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
ルーティングを作成する
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ファイルでは下記のように記載してくれます。
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
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ファイルにアソシエーションの記述をすると下記のようになる。
class User < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
end
class Room < ApplicationRecord
belongs_to :user
belongs_to :room
end
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
end
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
ルーティングを作成する
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コントローラー
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コントローラー
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
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