RailsでややこしいDM機能を1万字でくわしく解説してみた

# RailsでDM機能を実装する方法

フォロー機能を実装後、相互フォロー時にメッセージできるDM機能を作りたいと思い下記の記事を参考に、実装しました。

https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03

実装はできたのですが、初心者の自分にとっては理解が追いつかずただコードを写しただけとなってしまったので、今回細かく噛み砕いて自分なりに説明したいと思います。


もし、Rails初心者の方で、DM機能を実装したはいいけど理解が追いついていないという人は、参考にしてみてください。


また、このDM機能は、フォロー機能後に実装しているので、そちらを知りたい方は下記の記事を参考にしてください。
https://qiita.com/rui-watanabe/items/e15f38fb2ca298012684

## テーブル設計を考える
テーブル設計は下記のようになります。

![demo](https://gyazo.com/d252cdee9789ce0692ff3a6e7748c33b/raw)

DM機能には、この4つのテーブル、Usersテーブル、Roomsテーブル、Entriesテーブル、Messagesテーブルを使います。

まず、２人のユーザーがチャットルームでメッセージをやりとりする、といったイメージを持ってください。

これを頭に入れた上で、この4つのテーブルについて解説していきます。

まず、ユーザーを管理するUsersテーブルがあります。

ユーザーは、Roomsテーブルというチャットルームに属していて、相互フォロワー同士で一つずつチャットルームが作られていきます。

そのため、このチャットルーム１つに入るユーザーは、複数（２人）というわけです。

1人1人のユーザーは他の人と相互フォロー関係になりたくさんの、チャットルームをもつ可能性があるので、UsersテーブルとRoomsテーブルは多対多の関係になります。

そのため、中間テーブルとしてEntriesテーブルをおき、その情報を管理します。

またRoomsでは、複数（2人）のユーザーが複数のメッセージを送る多対多の関係なので、これも中間テーブルとしてMessagesテーブルをおき、その情報を管理します。

具体的なアソシエーションの記述にすると以下の通りです。

```user.rb
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
```
```room.rb
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy
```
```entry.rb
  belongs_to :user
  belongs_to :room
```
```message.rb
  validates :content, presence: true
  belongs_to :user
  belongs_to :room
```

中間テーブルが2つあり少し複雑ですが、一番大切なところなので理解しておきましょう。

（Userモデルに記述した、validates :content, presence: trueというのは、メッセージが空白の場合、保存されないようにバリデーションをかけているといった記述になります。）



## ルーティングの設定

ルーティングは下記のような記述になります。

```routes.rb
resources :users, only: [:show,:edit,:update]
resources :messages, only: [:create]
resources :rooms, only: [:create,:show]
```

流れとしては、

①相互フォローとなったら、相手のusersのshowページで「チャットへ」というボタンを押すことができるようになる。

②押したと同時にroomsがcreateされる

③roomsのshowページに移る

④roomのshowページでmessageがcreateされる

といったイメージです。


## コントローラーとビューの設定

ここから少し複雑になっていくので、各テーブルごとに解説していきます。

### users_controller

まずusers_controllerは、下記のような記述になります。

```users_controller.rb
class UsersController < ApplicationController
  
  before_action :authenticate_user!, only: [:show]

  def show
    @user=User.find(params[:id])
    @currentUserEntry=Entry.where(user_id: current_user.id)
    @userEntry=Entry.where(user_id: @user.id)
    unless @user.id == current_user.id
      @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
      else
        @room = Room.new
        @entry = Entry.new
      end
    end
  end
```

めちゃくちゃややこしいですね笑

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

```
 @user=User.find(params[:id])
 @currentUserEntry=Entry.where(user_id: current_user.id)
 @userEntry=Entry.where(user_id: @user.id)
```
この部分は、showページのために、レコードからユーザー1人1人の情報を持ってくる必要があるため、findメソッドを使っています。

そしてroomがcreateされた時に、現在ログインしているユーザーと、「チャットへ」を押されたユーザーの両方をEntriesテーブルに記録する必要があるので、whereメソッドでそのユーザーを探しているということです。

```show.html.erb
unless @user.id == current_user.id
  @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
  unless @isRoom
    @room = Room.new
    @entry = Entry.new
  end
end
```
この部分は、まずはじめに、現在ログインしているユーザーではないというunlessの条件をつけます。

そして、すでにroomsが作成されている場合と作成されていない場合に条件分岐させます。

作成されている場合は、先ほどコードを書いた、 @currentUserEntryと@userEntryをeachで一つずつ取り出し、それぞれEntriesテーブル内にあるroom_idが共通しているのユーザー同士に対して@roomId = cu.room_idという変数を指定します。

これで、すでに作成されているroom_idを特定できるというわけです。

@isRoom=trueと記述したのは、これがfalseの時、つまりはRoomを作成するときの条件を記述するためです。

そのためunless @isRoom内では、新しくインスタンスを生成するために、.newと記載します。


### users/show.html.erb

続いて先ほど記述した、コントローラーの変数を実際にusersの/showページで反映させたいと思います。

```
<% unless @user.id == current_user.id %>
  <% if (current_user.followed_by? @user) && (@user.followed_by? current_user)  %>
  <% if @isRoom == true %>
    <p class="user-show-room"><a href="/rooms/<%= @roomId %>" class="btn btn-primary btn-lg">チャットへ</a>
  <% else %>
    <%= form_for @room do |f| %>
      <%= fields_for @entry do |e| %>
        <%= e.hidden_field :user_id, value: @user.id %>
      <% end %>
      <%= f.submit "チャットを始める", class:"btn btn-primary btn-lg user-show-chat"%>
    <% end %>
  <% end %>
  <% end %>
<% end %>
```

ここも区切って解説していきます。

```
<% unless @user.id == current_user.id %>
  <% if (current_user.followed_by? @user) && (@user.followed_by? current_user)  %>
  <% if @isRoom == true %>
    <p class="user-show-room"><a href="/rooms/<%= @roomId %>" class="btn btn-primary btn-lg">チャットへ</a>
  <% else %>
  <% end %>
  <% end %>
<% end %>
```

まず、現在ログインしているユーザーではないという条件をつけ、APIのメソッドを使って、相互フォロー状態の時という条件も付け足します。

そしてコントローラーと同様に、すでにチャットルームが作成している時と作成されていない時の条件分岐をさせるため、@isRoomを使用。

@isRoomがtrueの時は、チャットへボタンを出現させ、すでに作成されたチャットへと移行することができます。

```
<%= form_for @room do |f| %>
  <%= fields_for @entry do |e| %>
    <%= e.hidden_field :user_id, value: @user.id %>
  <% end %>
  <%= f.submit "チャットを始める", class:"btn btn-primary btn-lg user-show-chat"%>
<% end %>
```
falseの場合にはform_forを使って、コントローラにパラメーターを送るための記述をします。

レコードには、親モデルのRoomsテーブルと、子モデルのEntriesテーブル両方に保存する必要があるので、親モデルにform_forインスタンス変数、子モデルにfields_forインスタンス変数とします。

そして、Entriesテーブルのレコードにはuser_idを送る必要があるので、hidden_fieldで@user.idをvalueにおきます。

これで、roomsテーブルに保存されるための準備が整いました。


### rooms_controller
次にrooms_controller内の処理について解説していきます。

```rooms_controller.rb
class RoomsController < ApplicationController

  before_action :authenticate_user!

  def create
    @room = Room.create
    @entry1 = Entry.create(room_id: @room.id, user_id: current_user.id)
    @entry2 = 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
      @message = Message.new
      @entries = @room.entries
    else
      redirect_back(fallback_location: root_path)
    end
  end
end
```
まずcreate部分について解説していきます。

```
def create
  @room = Room.create
  @entry1 = Entry.create(room_id: @room.id, user_id: current_user.id)
  @entry2 = Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
  redirect_to "/rooms/#{@room.id}"
end
```

先ほど、users/show.html.erbのform_forの@roomで送られてきたパラメータを、ここで受け取りcreateさせます。

また、このcreateメソッドでは、Room以外にその子モデルのEntryもcreateさせなければいけないので、Entriesテーブルに入る相互フォロー同士のユーザーを保存させるための記述を行います。

まず、現在ログインしているユーザーに対しては、@entry1とし、EntriesテーブルにRoom.createで作成された@roomにひもづくidと、現在ログインしているユーザーのidを保存させる記述をします。

@entry2ではフォローされている側の情報をEntriesテーブルに保存するため。users/show.html.erbのfields_for @entryで保存したparamsの情報(:user_id, :room_id)を許可し、現在ログインしているユーザーと同じく@roomにひもづくidを保存する記述をしています。

そしてcreateと同時に、チャットルームが開くようにredirectをしています。

```
def show
  @room = Room.find(params[:id])
  if Entry.where(user_id: current_user.id,room_id: @room.id).present?
    @messages = @room.messages
    @message = Message.new
    @entries = @room.entries
  else
    redirect_back(fallback_location: root_path)
  end
end
```
roomsのshowアクションでは、まず１つのチャットルームを表示させる必要があるので、findメソッドを使います。

条件としてはまず、Entriesテーブルに、現在ログインしているユーザーのidとそれにひもづいたチャットルームのidをwhereメソッドで探し、そのレコードがあるか確認します。

もしその条件がfalseだったら、前のページに戻るための記述である、redirect_backを使います。

もしその条件がtrueだったら、Messageテーブルにそのチャットルームのidと紐づいたメッセージを表示させるため、@messagesにアソシエーションを利用した@room.messagesという記述を代入します。

また、新しくメッセージを作成する場合は、メッセージのインスタンスを生成するために、Message.newをし、@messageに代入させます。

そしてrooms/show.html.erbmでユーザーの名前などの情報を表示させるために、@room.entriesを@entriesというインスタンス変数に入れ、Entriesテーブルのuser_idの情報を取得します（ビューの方で記述）。


### rooms/show.html.erb

次にrooms/show.html.erb内のの処理について解説していきます。

```rooms/show.html.erb
<div class="left-button">
  <%= link_to "ユーザー一覧に戻る", "/users/index",class:"edit-link" %>
</div>
<h4 class="rooms-title">気になる同士</h4>
<% @entries.each do |e| %>
  <div class="user-name">
  <strong>
    <%= image_tag e.user.avatar, class:"user-image" %>
      <a class="rooms-user-link" href="/users/<%= e.user.id %>">
        <%= e.user.name %>さん
      </a>
  </strong>
  </div>
<% end %>
<hr>
<div class="chats">
  <div class="chat">
    <% if @messages.present? %>
      <% @messages.each do |m| %>
        <div class="chat-box">
          <div class="chat-face">
            <%= image_tag m.user.avatar, class:"user-image" %>
          </div>
          <div class="chat-hukidashi"> <strong><%= m.content %></strong> <br>
            <%= m.created_at.strftime("%Y-%m-%d %H:%M")%>
          </div>
        </div>
      <% end %>
    <% end %>
  </div>
  <div class="posts">
    <%= form_for @message do |f| %>
      <%= f.text_field :content, placeholder: "メッセージを入力して下さい" , size: 70, class:"form-text-field" %>
        <%= f.hidden_field :room_id, value: @room.id %>
          <%= f.submit "投稿",class: 'form-submit'%>
    <% end %>
  </div>
</div>
```

DM機能っぽくビューを見せるために、少しHTMLの記述量が多くなっています。

ここでは、先ほどrooms_controllerで記述したところをどう反映するのかについて、解説していきます。


```
<% @entries.each do |e| %>
  <div class="user-name">
  <strong>
    <%= image_tag e.user.avatar, class:"user-image" %>
      <a class="rooms-user-link" href="/users/<%= e.user.id %>">
        <%= e.user.name %>さん
      </a>
  </strong>
  </div>
<% end %>
```
前述した@entriesの相互フォロワー同士に情報をとってきたいため、eachでフォロ・フォロワーの情報を取得しています。

そしてEntriesテーブルはアソシエーションを組んでいるので、Entriesテーブルのuser_idのレコードを参照し、Usersテーブルのidとavatarとnameの情報をとってきています。

```
<div class="chat">
  <% if @messages.present? %>
    <% @messages.each do |m| %>
      <div class="chat-box">
        <div class="chat-face">
          <%= image_tag m.user.avatar, class:"user-image" %>
        </div>
        <div class="chat-hukidashi"> <strong><%= m.content %></strong> <br>
          <%= m.created_at.strftime("%Y-%m-%d %H:%M")%>
        </div>
      </div>
    <% end %>
  <% end %>
</div>
```
メッセージの表示の部分は、rooms_controllerで記述したインスタンス変数@messagesを使い、メッセージが入っているかどうかif文で確認します。

もしメッセージが入っていたら、そのメッセージの情報ごとに表示するため、eachメソッドを使います。

ここでも先ほど同じようにアソシエーションを組んでいるので、Messagesテーブルに紐づいたuser_idから、avatarの情報を表示させます。

また、Messagesテーブルに保存されている、メッセージ内容のcontentカラムの情報と、日本時間を示すcreated_at.strftime("%Y-%m-%d %H:%M")の情報も表示させています。

```
<div class="posts">
  <%= form_for @message do |f| %>
    <%= f.text_field :content, placeholder: "メッセージを入力して下さい" , size: 70, class:"form-text-field" %>
    <%= f.hidden_field :room_id, value: @room.id %>
    <%= f.submit "投稿",class: 'form-submit'%>
  <% end %>
</div>
```
そして、新しく作成するメッセージを保存させるためにform_forを使って、パラメーターとして送る記述を書きます。

form_forではユーザーが入力したメッセージの内容を取得するため、f.text_field :contentと置いています。

またここでは、どのチャットルームのメッセージが判断するために、f.hidden_fieldで：room_idのバリューとして、そのチャットルームでのidを取得しています。

そしてf.submitとすることで、投稿ボタンが押された時点で、messages_controllerにパラメータが飛んでいきます。

### messages_controller

最後に、messages_controllerでメッセージをcreateさせる記述をしていきます。

```messages_controller.rb
class MessagesController < ApplicationController

  before_action :authenticate_user!, only: [:create]
  
  def create
    if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
      @message = Message.create(params.require(:message).permit(:user_id, :content, :room_id).merge(user_id: current_user.id))
    else
      flash[:alert] = "メッセージ送信に失敗しました。"
    end
　　redirect_to "/rooms/#{@message.room_id}"
  end
end
```
ここではrooms/show.html.erbで送られてきた、form_forのパラメータを実際に保存させるための記述をします。

まず、form_forで送られてきたcontentを含む全てのメッセージの情報の:messageと:room_idのキーがちゃんと入っているかということを条件で確認します。

もしその条件がtrueだったら、メッセージを保存するためにMessage.createとし、Messagesテーブルにuser_id、:content、room_idのパラメーターとして送られてきた値を許可。

メッセージを送ったのは現在ログインしているユーザーなので、そのuser_idの情報をmergeさせます。

そしてその条件がfalseだった場合、フラッシュメーセージを出すための、flash.now[:alert]という記述をします。

最後にredirectで、どちらの条件の場合も元のページへとredirectさせれば完成です。


## DM機能は初心者にとっては複雑だが理解はできる

ここまで長々と解説してきたのですが、DM機能についてご理解いただけたでしょうか。

DM機能は4つのテーブルを使ったり、条件が多かったりと初心者の自分にとってはとても難しく感じました。

しかし一つずつ噛み砕いていけば、基礎的なことが多いので理解することは可能だと思います。

RailsのアプリケーションでSNS系のアプリケーションを作られている方は、ぜひ参考にしてみてください。