LoginSignup
3
4

More than 3 years have passed since last update.

「Rails」「初心者向け」ダイレクトメッセージ機能を1行ずつ解説

Last updated at Posted at 2020-03-20

初めに

この記事は、Railsでダイレクトメッセージ機能を実装する方法について紹介します。
私自身が実装するにあたり、参考にさせて頂いた記事があります。
したがって、この記事の大部分は、それらの記事の内容に依っていますが、一部、私自身のしたいことがあり、応用した部分もあります。

この記事は、Railsチュートリアルを終えたレベルの初心者向けの記事です。
したがって、中級者以上の方が読まれると、冗長な文章に感じられると思います。ただ、初心者にとっては、当然の前提とされていることが、解説されていないことで詰まることは多いので、なるべく詳しく書こうと考えています。

参考文献
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
https://iberiko665.hatenablog.com/entry/2019/03/03/215730

ダイレクトメッセージとは...

そもそも、私はダイレクトメッセージが、投稿やチャットとどのように異なるのか理解していなかったので、一応整理しておきます。私としては以下のように分類しています。

  1. 投稿
    誰でも参加できる。誰でも投稿された文章、画像を見ることが出来る。
  2. ダイレクトメッセージ
    基本的に一対一や少数で他から見えない形で部屋が作られ、コメント出来る。
  3. チャット
    通常のHTTP通信と違い、web socketを使用している。 大きな違いとして、ユーザーがリロードせずに保存されたデータが表示される仕組みにできる(リアルタイム性がある)。

実装例

商品詳細画面(products/show)から「チャットを始める」ボタンを押すと、新たにルーム(rooms/show)が作られます。そのルームで、一対一でコミュニケーションを取ることが出来ます(「チャット」としていますが、上記の定義から言えば、「ダイレクトメッセージ」となります)。
app/views/products/show.html.erb
スクリーンショット 2020-03-18 7.26.08.png

app/views/rooms/show.html.erb
スクリーンショット 2020-03-18 7.43.29.png

注意

この画像はあくまでイメージです。
添付の画像は、私が制作したアプリの一部をスクリーンショットしたものです。
今回、添付の画像のようなデザイン(CSSやdiv要素)についての部分のコードは載せず、解説はしていません。
また、ゼロから始めるスタイルを採らず、重要な箇所だけを重点的に解説するスタイルを採ります。
書いていて、あまりに長大になったため、そこまで時間が取れませんでした。

ダイレクトメッセージ機能を、どのように実装するのか、何となくイメージを掴んでいただくことを目的としています。

モデリング

Productモデル、Userモデル、Roomモデル、Membershipモデル、Messageモデルの5つを使用します。
それぞれの関係性は以下の通りです。
Userモデル
実際にメッセージを送る主体。
Product(商品)を出品し、Room(ルーム)でMessage(メッセージ)を送り合います。

Productモデル
ユーザーによって出品された、商品を指します。
メッセージのやり取りは、「その商品」についてコミュニケーションを取りたい時に行うことが想定されています。

Roomモデル
各商品について、ユーザー間の関係性に応じて新たに作られるページを指します。
例えば、ユーザーAとユーザーBがメッセージを交わすために、一つのルームが作られますし、また、ユーザーAとユーザーCがメッセージを交わそうと思えば、別の新たなルームが作られます。

Membershipモデル
Roomモデルにおける関係性を指します。
具体的には、新たにルームが作られると、同一のroom_idを持つ、二つのMembershipモデルが作成されます。
実際のコードを見ながらの方が理解しやすいと思います。

Messageモデル
Roomモデルによって作られたルームで、ユーザー同士で交わされるメッセージを指します。

関連

あらかじめ、Railsチュートリアルで構築した認証機構や、sorcery, deviseといったgemを使用して、「現在ログインしているユーザー」を表すcurrent_userを作成していることが前提となります。
また、前述のモデルを作成していることが前提となります。

関連は以下の通りです。

user.rb
has_many :products
has_many :messages, dependent: :destroy
has_many :memberships, dependent: :destroy
product.rb
belongs_to :user
has_one :room
room.rb
has_many :messages, dependent: :destroy
has_many :memberships, dependent: :destroy
belongs_to :product
membership.rb
belongs_to :user
belongs_to :room
message.rb
belongs_to :user
belongs_to :room

カラムについては、確実に必要なのは、Messageモデルのcontentカラムです。
また、関連のためのforeign_keyも作成する必要があります。
foreign_keyがピンと来ない方は、下記の記事が分かりやすいです。
https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1

本記事のモデリングでは、Productモデルは、user_idを、Roomモデルは、product_idを、Membershipモデル及びMessageモデルは、user_id, room_idをforeign_keyのカラムとして持たせる必要があります。

全体像の把握

データがどのように渡っていくか、この実装の骨子は以下の通りです。
productsコントローラーshowアクション =>
products showテンプレート =>
roomsコントローラーcreateアクション =>
roomsコントローラーshowアクション =>
rooms showテンプレート =>
messagesコントローラーcreateアクション =>

productsコントローラーshowアクション =>
products showテンプレート =>
(roomsコントローラーshowアクション)

以下、一つずつ解説していきます。

products#show - すでにルームを作成しているかのチェックを行う

products_contrller.rb
def show
 @product = Product.find(params[:id])
 @user = @product.user
  if current_user
    @current_user_memberships = Membership.where(user_id: current_user.id)
    @current_user_memberships.each do |current_user_membership|
      if current_user_membership.room.product_id == @product.id
        @has_room = true
        @room_id = current_user_membership.room_id
      else
        @room = Room.new
        @membership = Membership.new
      end
    end
  end
 end

1行ずつ解説していきます。
@product = Product.find(pramas[:id])
商品詳細ページなので、その商品のidをfindメソッドで取得して、@productという変数に代入しています。
その商品に紐づいたルームを作るために必要です。チュートリアルでもおなじみの記述だと思います。

@user = @product.user
これは、「その商品を出品したユーザー」を取得したいので、記述しています。products/showテンプレートで使用しています。
has_many, belongs_toなどで関連を作っていると、このように「モデル.モデル」とすることで、そのモデルと紐づいた別のモデルのデータを取得することが出来ます。

if current_user
これは、「現在ログインしているユーザーならば」、以下のif文に進むという意味です。
もし、ログインしていないならば、このshowアクションの場合、そのまま終わります。私の設計では、ダイレクトメッセージはログインしているユーザーだけに限定しています。
ログインしているユーザーに限定する理由は、一度でもルームを作成したことがあるかどうかを判断するために必要だからです。
詳しくは後の行で説明します。

@current_user_memberships = Membership.where(user_id: current_user.id)
この行で、行なっていることは、Membershipモデルから、現在ログインしているユーザーが保存されている関係性があるかをwhereメソッドで検索して、もしあれば、変数に代入しています。
なぜ、そのようなことをしているのかというと、一度でもルームを作成したことがあるかどうかを確かめ、もしあるならば、作成してあるルームへ、作成したことがないのなら、新たに作成するという条件分岐を記述するためです。
まずは、当然、作成されていないので、この行は考える必要はありません。
一通り見た後でないと、この行の意味は分からず、混乱すると思うので、今は理解できなくて結構です。
最初は、Membershipモデルにはデータが存在しないので、この変数には[](空の配列)が入ります。

@current_user_memberships.each do |current_user_membership|
この行からeach文で繰り返しを行なっています。前の行と同様に今は深く考える必要はないです。
実際の値としては、前の行の[]がそのまま、current_user_membershipという変数に入る、と考えてください。

if current_user_membership.room.product_id == @product.id
この行で場合分けを行います。具体的には、現在ログインしているユーザーの関係性を持つルームのproduct_idと、商品詳細ページのその商品のidが等しいかどうかで場合分けを行なっています。
等しいならば、trueとして@has_room..の行に進みますが、
今はcurrent_user_membership.room.product_idに値がないので、falseです。
else以下に進みます。

@room = Room.new
この行で、Roomモデルのオブジェクトを作成しています。オブジェクトといっても分かりづらいでしょうが、全ての値がnilのモデルが作成されているはずです。以下の画像のように作成されます。
今、@roomには、その作成しただけの値の何も入っていない型を代入しています。
Room.new
スクリーンショット 2020-03-18 8.55.26.png

@membership = Membership.new
この行も同様です。

products/show.html.erb

show.html.erb
<div class="chatBox">
  <% if current_user %>
    <% if @has_room %>
      <div class="already-has-chatting">
        <%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
      </div>
    <% else %>
      <div class="start-chat">
        <%= form_with url: rooms_path, method: :post, local: true do |f| %>
          <%= hidden_field_tag :product_id, @product.id %>
          <%= hidden_field_tag :user_id, @user.id %>
          <%= f.submit "チャットを始める", class: "start-chat-button" %>
        <% end %>
      </div>
    <% end %>
  <% end %>
</div>

product/show.html.erbでしていること
すでにルームを作成したことがあるならば、そのルームのページを開きたいので、GETメソッドを/room/:idに送信する。
まだ、ルームを作成したことがないならば、ルームを作るため、form_withを使い、POSTメソッドを/roomsに送信する、ということです。

以下、説明していきます。
<% if current_user %>
この行は、先のコントローラーと同様に、現在ログインしているユーザーを想定してダイレクトメッセージ機能を実装しているので、記述しています。

<% if @has_room %>
この行は、products#showで、もしすでに作成したルームがあるならば、showアクションで、trueという値を@has_roomという変数に代入しているのですが、現時点では作成していないので、falseです。
したがって、このif文はelseに進みます。
今は深く考える必要はないです。

<%= form_with url: rooms_path, method: :post, local: true do |f| %>
このform_withは、Products#showでの値を、次のRoomsコントローラーへ渡すことが目的です。
Railsチュートリアルでは、form_for(@user)のような使用がメインだったかと思いますが、現在、form_withを使用することが推奨されているため、form_withを使用していきましょう。

form_withでは、urllocalを指定する必要があります。urlは送信先の名前付きルートを、methodはGETかPOSTか、DELETEかといった種類を指定するモノです。localというのは、Ajaxという機能を使用するのかを指定するもので、今回は使用しないので、local: trueとします。

<%= hidden_field_tag :product_id, @product.id %>
この行は、products#showで取得した商品のidを次のroomsコントローラーへ引き継ぎたいので、記述しています。

<%= hidden_field_tag :user_id, @user.id %>
この行も同様に、@user = @product.userで取得していた@userの値を引き継ぎたいので、記述しています。

<%= f.submit "チャットを始める", class: "start-chat-button" %>
この行は、ボタンが押されると、hidden_fieldで渡した値がroomsコントローラーへ行くように指示しています。


hidden_fieldについて
hidden_fieldとhidden_field_tagの違いや、使い方について補足したいと思います。
それは、私自身がダイレクトメッセージ機能を実装するまで、両者の違いをうまく理解できていなかったと思うからです。
まず先ほど解説したフォームでは、hidden_field_tagを使用しましたが、
その理由は「モデルに紐づく形で値を渡す必要がないから」です。

文章で書いても分かりづらいと思うので、実際の値を見てみましょう。
まず、hidden_field_tagで渡したこのproducts/show.html.erbでのparams(パラメータ)は以下の通りです。
スクリーンショット 2020-03-18 10.26.16.png

全体像の把握の章でも説明したように、このチャットを始めるボタンを押した際、送信先はroomsコントローラーのcreateアクション(rooms#create)です。
したがって、値を見るためにrooms#createにてbinding.pryで確かめています
(binding.pryはデバック方法の一つです)。

この赤字のパラメータの内容が、今、取り上げたい箇所です。
全ての値が一つの{}(ハッシュ)の中にKey, Value(product_idというKeyと、3というValueなど)という形式で格納されていることがお分かりいただけると思います。
つまり、一緒くたにされているということです。
これが、hidden_field_tagを使用した際の、値の渡り方です。

次に、hidden_fieldを使用した場合には、どのように値が渡るのか確かめてみましょう。

<%= form_with model: product, url: rooms_path, method: :post, local: true do |f| %>
   <%= f.hidden_field :product_id, value: product.id %>
   <%= f.hidden_field :user_id, value: user.id %>
   <%= f.submit "チャットを始める", class: "start-chat-button" %>
<% end %>

先ほど解説したform_withの引数にmodel: productとしている点、f.hidden_fieldhidden_field_tagであった部分を変更している点が異なります。

このように変えた場合、どのように値が渡るのかというと、以下のように渡ります。
スクリーンショット 2020-03-18 10.37.17.png

注目していただきたいのは、"product"=>{ "product_id"=>"1", user_id"=>"1"}となっている部分です。
このようにhidden_fieldを使用すると、modelとして指定した値の内部にhidden_fieldで指定した内容が格納されます。そのため、f.hidden_fieldform_withのフォームビルダーをレシーバとして記述する必要があります。
つまり、(product)モデルに紐づく形式で値が渡っているということです。

-補足-
フォームビルダーとは、form_withなどdo-endで囲まれた部分をブロックと呼びますが、そのブロック内部で|f|と設定しているもののことです。
レシーバとは、User.findのような記述でのUserに当たるモノです。findというメソッドを受ける対象と考えると分かりやすいです。

結論として、今回のproducts/show.html.erbからrooms#createへはモデルに紐づく形で値を渡す必要がなかったので、hidden_field_tagを使用しています。

さらに、これは余談ですが、hidden_fieldhidden_field_tagも第一引数は「どのように値を受け取りたいか」、第二引数は「渡す値」を指定します。
私は、これまでform_withf.text_fieldなどでも、テーブルのカラムを渡すものと思っていたのですが、これは勘違いでした...。
例えば、Railsチュートリアルでは、User登録の際に

form_for(@user) do |f|
 f.text_field :name
 f.submit
end

このような、記述で値を更新したと思いますが、てっきり、Userモデルを作成した際に、nameカラムも作成したので(rails g model user name:string ..のこと)、そのカラム名をf.text_field :nameとして指定しなければならないと思っていました(https://railstutorial.jp/chapters/sign_up?version=5.1#sec-using_form_for)。

これは、別にf.text_field :happyでもなんでも良いです。その際に、値を受け取る方で、params[:happy]として受け取れば良いだけです。
「値をどのように受け取るか」を指定しているだけなので、自由に決めて良いです。
もし私と同じように勘違いされている方がいらっしゃれば、色々binding.pryで試してみられると良いかと思います。

rooms#create - 一つのRoomモデル、二つのMembershipモデルの作成・保存

さて、products/show.html.erbから渡された値は、roomsコントローラーのcreateアクションに送信しているので、その部分の説明をします。

rooms_controller.rb
def create
    @room = Room.new
    @room.product_id = params[:product_id]
    @room.memberships.build(user_id: params[:user_id])
    @room.memberships.build(user_id: current_user.id)
    @room.save
    redirect_to @room
end

rooms#createでしていること
その商品に紐づいたルームを作りたいので、Roomモデルに先の引き継いだproduct_idの値を代入していること、また、そのルームに紐づく関係性(Membershipモデルを指す)を二つ作成していることです。
関係性という考え方が難しいと思いますが、じっくり追っていけば理解できると思うので、頑張りましょう。

@room = Room.new
この行では、改めてRoomモデルの型を作成しています。
ただ、作成しただけなので、ここであまり考えすぎないようにしてください。
作成したRoomモデルにそれ以降の行で、データを登録していきます。

@room.product_id = params[:product_id]
この行では、先のhidden_field_tagで引き継いだ、商品詳細ページの「その商品」のidを取得しています。
params[:product_id]とすることで、先ほど紹介したパラメータの中で、product_idというKeyに対応するValueを取得できます。
その値を@room.product_idに代入しています。Roomモデルのproduct_idカラムに代入しているということになります。

スクリーンショット 2020-03-18 11.55.49.png

コードの出力から見ると、@roomにはproduct_idというKeyだけ値が入っています。
idの6という数字は、スクショを取るためのこちらの都合なので、気にしないで良いです。

@room.memberships.build(user_id: params[:user_id])
この行も、引き継いだユーザーのidをparams[:user_id]として受け取り、membershipsモデルのuser_idカラムに代入しています。
products/show.html.erbから渡されたuser_idは、「その商品を出品したユーザー」のidです。
次の行の@room.memberships.build(user_id: current_user.id)は今操作している「現在ログインしているユーザー」のidです。

この2行でしていることは、それぞれのparams[:user_id]と、current_user.idをMembershipモデルのuser_idとして登録し、さらにMembershipモデルのroom_idはこのrooms#createの最初の行で作成したRoomモデルを代入しています。

そうすることで、同じルームのidを持ったuser_idが異なるMembershipモデルが二つ作られます。
ここで、作成したMembershipモデルを、今は気にしないで良いですと言った最初の、products#showにて、使用することで、すでにルームを一度でも作成したことがあるのかを確かめます。

この部分は、ダイレクトメッセージ機能の中でも難しい部分なので、自分なりに考えてみて下さい。

@room.memberships.buildという記述は、RoomモデルとMembershipsモデルに関連がある場合、「そのRoomモデルに紐づくMembershipモデルの型を作成する」という意味です。
MembershipモデルはRoomモデルに従属する関係なので、「そのRoomモデルに紐づく」という部分を加えて、記述する必要があります。

@room.save
この行は、Roomモデルと、Membershipモデルで、代入してきた値を保存しています。

redirect_to @room
rooms#showへリダイレクトさせています。この記述の仕方は、下記の記事が詳しいです。
https://qiita.com/Kawanji01/items/96fff507ed2f75403ecb


rooms#show - ルームのこれまでのメッセージの取得及びMessageモデルの作成

rooms_controller.rb
def show
  @room = Room.find(params[:id])
  if Membership.where(user_id: current_user.id, room_id: @room.id).present?
    @messages = @room.messages
    @message = Message.new
  else
    redirect_back(fallback_location: root_path)
  end
end

rooms#showでしていること
1. rooms#createで作成したRoomモデルに紐づいたmessagesがすでにあるなら、それを取得すること。
2. 新しくメッセージをするので、Messageモデルを作成する、ということです。

以下、1行ずつ解説していきます。

@room = Room.find(params[:id])
この行は、rooms#createで、@room.saveとした時に、登録されたRoomモデルのidを取得することで、「そのルーム」という部分を検索しています。

if Membership.where(user_id: current_user.id, room_id: @room.id).present?
この行は、先のrooms#createで登録したMembershipモデルをwhere文を使うことで、検索しています。
そして.present?メソッドが続くので、「もしあるならば」進みなさい、という意味になります。

つまり、保存された全てのMembershipモデルを参照して
1. user_idが「現在ログインしているユーザー」のモノで、
2. room_idが、今findで取得したルームのidのroom_id
というMembershipモデルがあるか、検索しています。

この行は、ルームを作成していないユーザーがそのルームに進入することを防ぐ役割があります。

where文の使い方は、多くの記事がありますし、関連するモデルを検索すると言った場合でない限り、さほど難しくないと思うので、簡単に説明して終えようと思います。
where文は、User.where(name: "nanasi")のように記述し、役割としては、今まで説明してきたように、その「モデルのデータを検索すること」です。findやfind_byに近い役割を持ちます。
文法は、モデル.where(カラム: 検索したい内容)です。
上の記述なら、Userモデルの中から、"nanasi"というnameのUserがいないか検索することが出来ます。

@messages = @room.messages
「そのルーム」でこれまで、作成されてきたメッセージを取得するために、記述しています。
@messagesという変数に代入しておいて、テンプレートでeach文を回すことで、これまでに保存されてきたメッセージを表示します。そのための布石です。

@message = Message.new
この行は、Messageモデルを作成しています。これまで何度も見てきたので、大丈夫だと思いますが、殻を作成しただけで、中身はありません。とりあえず、型だけを作成しておきます。

else文の方の
redirect_back(fallback_location: root_path)
は、root_pathへリダイレクトさせます。

rooms/show.html.erb - メッセージとフォームの表示

show.html.erb
<div class="container">
  <div class="row">
    <%= render "message_area", messages: @messages %>
  </div>
  <div class="row">
    <div class="col-md-8 offset-md-2" >
      <%= form_with model: @message, url: messages_path, method: :post, local: true, html: { class: "message-form"} do |f| %>
        <div class="input-message">
          <%= f.text_area :content, class: "content" %>
        </div>
        <%= f.hidden_field :room_id, value: @room.id %>
        <%= f.submit "+", class: "btn" %>
      <% end %>
    </div>
  </div>
</div>
_message_area.html.erb
<div class="col-md-8 offset-md-2">
  <div class="message-area">
    <% if messages.present? %>
      <% messages.each do |message| %>
      <div class="message-one">
        <% user = message.user %>
        <% class_suffix = current_user == user ? "right" : "left" %>
        <div class="user-icon-<%= class_suffix %>">
          <% if message.user.image.url(:small) %>
            <%= image_tag message.user.image.url(:small) %>
          <% else %>
            <%= message.user.name %>
          <% end %>
        </div>
        <div class="balloon-<%= class_suffix %>">
          <div class="balloon-content"><%= message.content %></div>
          <div class="timestamp">
            <%= time_ago_in_words(message.created_at) %>前
          </div>
        </div>
      </div>
      <% end %>
    <% end %>
  </div>
</div>

rooms/show.html.erbでしていること
1. 今までそのルームで行われてきた、メッセージを全て表示すること
2. 新たにメッセージを作成するために、フォームから値を渡すこと
です。

1行ずつ解説していきます。

<%= render "message_area", messages: @messages %>
この行は、メッセージの表示部分をパーシャルとして切り出したので、その記述です。
rooms#showで@messagesという変数に、「そのルームの」今までの全てのメッセージを取得してあるので、その変数を使用します。

まず、このパーシャル内部を先に説明します。
<% if messages.present? %>
この行で、これまでそのルームで投稿されたメッセージがあるかどうかをチェックしています。

<% messages.each do |message| %>
この行では、rooms#showで取得した@room.messagesをeach文で繰り返しています。
こうすることで、すでに投稿されたメッセージの内容を全て取得することができます。

<% user = message.user %>
この行は、「そのメッセージを投稿したユーザー」を取得しています。この形式で記述することでUserモデルを取得できることは、すでに説明したと思います。
このように、userという変数に代入する目的は自分と相手の投稿を判断して、相手の投稿であれば、右から吹き出しを出現させ、自分の投稿であれば、左から出現させるというデザイン上のものです。
したがって、特にこだわらないという場合は不要です。

<% class_suffix = current_user == user ? "right" : "left" %>
この行も同様にデザイン上の記述です。
現在ログインしているユーザーが前の行で記述した「そのメッセージを投稿したユーザー」ならば、右から、そうでないならば左から吹き出しが出るように設計しています。

<div class="balloon-content"><%= message.content %></div>
何行か飛ばした後、この行で、今まで保存されてきたメッセージを表示しています。

このパーシャルで、重要な部分はこのmessage.contentぐらいで、後は、ほとんどデザインの問題なので、説明を省いた部分が多いですが、デザイン部分に興味がある方は、私が参考にさせて頂いたサイトをご覧になって下さい(https://iberiko665.hatenablog.com/entry/2019/03/03/215730 )。

さて、パーシャルが終わったので、
次は、この行です。
<%= form_with model: @message, url: messages_path, method: :post, local: true, html: { class: "message-form"} do |f| %
フォームを作成しています。このフォームにより、新規のメッセージを投稿できるようにしています。
html:というシンボルの部分は、ハッシュを渡していますが、これもclassを定義することで、CSSを当てるためなので、重要ではありません。

このフォームの送信先は、messages#createです。
また、前回のform_withと異なり、modelというシンボルを設定しています。
これは後の行で「messageモデルの値を更新したいので」、このように記述しています。
modelに紐づく形で、値を更新したい場合は、このようにmodelを設定する必要があります。

<%= f.text_area :content, class: "content" %>
この行は、ユーザーにメッセージを記述してもらう部分をtext_areaを使用して、作成しています。

<%= f.hidden_field :room_id, value: @room.id %>
この行は、先に説明したhidden_fieldを使用しています。
Messageモデルに紐づいた形で、値を渡したいので、このように記述しています。
hidden_fieldとして渡している値は、@room.idです。
「そのルームの」メッセージとして保存したいので、渡す必要があります。
この行、そしてこのフォームについては、最後のmessages#createとも密接に関わっているので、以上の説明だけではなく、messages#createの説明と合わせて読んでいただくことで理解できるかと思います。

rooms/show.html.erbの説明は以上です。

messages#create - 新規メッセージの作成・保存

messages_controller.rb
class MessagesController < ApplicationController
  def create
    if Membership.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
      @message = Message.create(message_params)
      redirect_to room_path(@message.room_id)
    else
      redirect_back(fallback_location: root_path)
    end
  end

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

messages#createでやっていること
このアクションでしていることは、シンプルで、投稿されたメッセージを保存することだけです。

1行ずつ解説していきます。

if Membership.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
この行は、Membershipモデルから、適切なuser_idとroom_idなのかどうかをチェックしています。
rooms#showでも、同様のif文があったので、大丈夫だと思います。

@message = Message.create(message_params)
この行で、投稿されたメッセージを保存しています。
createメソッドは、作成と保存を同時に行うことができるメソッドです。
つまり、rooms#createでは、Room.newとしてオブジェクトを作成し、最後に@room.saveとして保存しましたが、それを一気にします。
複雑なのは、ストロングパラメータでmergeメソッドが使用されている点です。

params.require(:message).permit(:user_id, :room_id, :content).merge(user_id: current_user.id)
ストロングパラメータ自体は、チュートリアルでもよく解説されていますし、params.require(:--).permit(:--,)という形式に抵抗はないと思いますが、その後のmergeとして続いている部分が私としては難しく、ストロングパラメータとmergeメソッドの組み合わせについてきちんと解説している記事を私自身が見つけられなかったこともあり、詳しく解説していきたいと思います。

まず、改めてですが、前回のform_withhidden_fieldを使用し、hidden_field_tagを使用しなかった理由から説明します。
それはMessageモデルに紐づく形で値を渡したかったからです。
つまり、値は
スクリーンショット 2020-03-18 20.39.48.png
このように、"message"=>{ "content"=>"Goodby", ... }といった形式で渡したい。
なぜなら、ストロングパラメータとして更新するにはそのように形式が整っていないと更新できないからです。
ここまでは、前回のhidden_fieldhidden_field_tagの違いをよく考えてみれば、分かると思います。

さらに、Messageモデルはcontent、user_id、room_idの三つのカラムのデータを更新する必要があります。
contentはメッセージの内容、user_idはどのユーザーが投稿したメッセージか、room_idはどのルームで投稿されたのか、という情報でMessageモデルには全て必須です。

しかし、スクショを見ると、"message"=>以下のハッシュにcontent, room_idはありますが、user_idがありません。したがって、このままでは、値が更新できず、
スクリーンショット 2020-03-18 20.50.14.png
このスクショのように、ROLLBACK、つまり何か不具合があったので、保存しませんというサインが出ています。

したがって、「user_idも同時に更新したいので、この値を入れてください」とお願いするために、mergeメソッドを使用します。
mergeメソッドの公式的な説明は「ハッシュの結合」です。
つまり、ストロングパラメータとしては、"message"=> { ... }というMessageモデルのハッシュに、user_idを強引にカットインさせるために使用します。
具体的には、
.merge(user_id: current_user.id)とすることで、そのハッシュに入れることが出来ます。
それはbinding.prymergeメソッドを記述した際、記述しなかった際を比較してみると分かると思います。

スクリーンショット 2020-03-18 20.57.37.png

このように、mergeメソッドを使用すると、パラメータの内部にうまく入れることが出来ました。

あとは、リダイレクトだけなので、説明は省きます。

products#show - 一度ルームを作成している場合

これから、すでにルームを作成し、再び同じルームに入る場合の処理について解説していきます。
もう一度該当のコードを載せます。

products_controller.rb
def show
 @product = Product.find(params[:id])
 @user = @product.user
  if current_user
    @current_user_memberships = Membership.where(user_id: current_user.id)
    @current_user_memberships.each do |current_user_membership|
      if current_user_membership.room.product_id == @product.id
        @has_room = true
        @room_id = current_user_membership.room_id
      else
        @room = Room.new
        @membership = Membership.new
      end
    end
  end
 end

rooms#createを思い出して頂きたいのですが、すでにルームを作成したことがあれば、Membershipモデルには、「現在ログインしているユーザー」のidが、user_idとして登録され、保存されています。

@room.memberships.build(user_id: current_user.id)
(rooms#create)

ここでは、whereメソッドを使い、Membershipモデルから、その関係性を全て取得しています。
全て取得しているというのは、ポイントの一つです。
それは、「現在ログインしているユーザー」は、他の商品のページで新たにルームを作成している場合も考えられるからです。
とりあえず、「現在ログインしているユーザー」が関わっているMembershipモデルを全て取得したい。
さらに言えば、「現在ログインしているユーザー」が関わっているroom_idを取得したい、ということです。
それを変数に代入します。

次に、その値をeach文で全て検討します。
if文のcurrent_user_membership.room.product_idという長いメソッドチェーンにより、「現在ログインしているユーザーをuser_idとしたMembershipモデルに紐づいたルームの商品のID」が取得できます。
メソッドチェーンとは、メソッドをつないでいったものです。

このメソッドチェーンは分かりづらいと思いますが、このように考えてみてください。
まず、現在ログインしているユーザーをuser_idとするMembershipモデルは多くある
=>
それら一つ一つのMembershipモデルが作成されるときに同時に作成したRoomモデルを探す
=>
そのRoomモデルを作成するときに、hidden_fieldで引き継いだproduct_idは、つまり「その商品」のページからボタンをクリックした「その商品のid」を指す
=>
したがって、 @product.idと等しい

このif文によって、現在ログインしているユーザーが一度でも、その商品について、あるルームを作成したことがあるかどうかがチェックされ、一度でもあるならばtrueとして進みます。

@has_room = true
この行では、前述のチェックのサインとして真偽値のtrueを変数に代入しています。
変数名に大きな意味はありません。
product/show.html.erbにて、条件式で使うため、設定しています。

@room_id = current_user_membership.room_id
この行は、先のif文で選抜された唯一のcurrent_user_membershipという変数から、room_idを取得し、代入しています。
@current_user_memberships = Membership.where(user_id: current_user.id)
序盤のこの行をみて分かるように、current_user_membershipという変数は、結局Membershipモデルだということを確認しておけば、難しくはないと思います。
この行の意味については、前の行と同じくshow.html.erbにて使用するためです。

product/show.html.erb - 一度ルームを作成している場合

product/show.html.erb
<div class="chatBox">
  <% if current_user %>
    <% if @has_room %>
      <div class="already-has-chatting">
        <%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
      </div>
    <% else %>
      <div class="start-chat">
        <%= form_with url: rooms_path, method: :post, local: true do |f| %>
          <%= hidden_field_tag :product_id, @product.id %>
          <%= hidden_field_tag :user_id, @user.id %>
          <%= f.submit "チャットを始める", class: "start-chat-button" %>
        <% end %>
      </div>
    <% end %>
  <% end %>
</div>

<% if current_user %>
最初の行である、この行では、「現在ログインしているユーザー」であるか?をチェックしています。
この点は大丈夫だと思います。

<% if @has_room %>
この行は、showアクションでtrueを代入しているため、trueとして進みます。
このように、すでにルームを作成しているかどうかのチェックのために使用します。

<%= button_to "チャットを再開する", room_path(@room_id), method: :get, class: "restart-chat-button" %>
この行は、ボタンタグを利用したリンクです。
ルームを作成していなかった場合には、rooms#createへ行き、そこで、Roomモデル、Membershipモデルを作成し、rooms#showへリダイレクトさせました。
今回は、すでにルームがあり、そのidも知っている状態なので、room_path(@room_id)として、該当のルームへのリンクとしています。

@room.idではなく、@room_idである点に注意してください。
この@room_idはshowアクションで最後に値を代入して変数のことです。

rooms#showが、すでにルームを作成している場合、新たにルームを作成する場合の合流地点になるので、あとは同じです。

最後に

以上で、ダイレクトメッセージ機能の説明は終わりです。

もともと私がダイレクトメッセージ機能を知ったのは、@nojinojiさんの記事
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
からです。
多くのモデルを組み合わせることにより、決められたユーザー同士だけのルームを作り、別のユーザーなら新たなルームが作られる。写経し、1行ずつ意味を考えてみて、その精緻さに感動しました。

私は、ある商品に紐づく形でダイレクトメッセージ機能を実装したいと思っていました。
人それぞれ、実装したい形は異なっていると思うので、この記事がそうした応用に役立てばいいと思い、共有します。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4