Rails DM機能の実装

# はじめに
今回はXクローンの実装にてDM機能の実装をしたので、自分自身の備忘録として、また今後フォロー機能を実装する方のお役に立てればと思い今回も記事を書くことにしました。

# DM機能の作成
要件として、ユーザー同士（1対1）でメッセージを送り合うことができる機能を実装する。
※ 前提として、ユーザー登録がすでに実装されているものとします。

# DM機能の流れ
相手ユーザーの詳細ページorポストのメニューorサイドバーのメニューからチャットに進む
↓
roomがなければ新規で作成され、そうでなければ存在するroomに遷移する
↓
roomでメッセージを送ることができるようになる

このように至って普通なDM機能を実装していきたいと思います。


# DM機能について
DM機能では、usersテーブル、roomsテーブル、entriesテーブル、messageテーブルを使用します。

2人のユーザーがチャットルームでメッセージをやりとりするというイメージを思い浮かべながら実装していきます。

まず、ユーザーを管理するusersテーブルがあります。
ユーザーはroomsテーブルというチャットルームに属していて、2人で1つのチャットルームが作成される。

ユーザー1人1人は、色々なユーザーとチャットをするので、たくさんのチャットルームをもつ可能性がある。
なので、usersテーブルとroomsテーブルは多対多の関係になる。

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

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

#### ER図に表すと
![スクリーンショット 2024-03-11 18.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/f74cf41a-ae87-a926-2fd0-94d97d396d9a.png)

説明をER図に表すと上のようなER図になります。


# モデルの作成
userは既に作成済みなので、残りのroom,entry,messageのモデルを作成する。

```
rails g model room
```
```
rails g model entry user:references room:references
```
```
rails g model message user:references room:references message:text
```

```
rails db:migrate
```

# 関連付けの設定
それぞれのモデルの関連付けの記述をしていこうと思います。

```ruby:user.rb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
```

```ruby:room.rb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
```

```ruby:entry.rb
  belongs_to :user
  belongs_to :room
```

```ruby:message.rb
  validates :message, presence: true
  belongs_to :user
  belongs_to :room
```

# ルーティングの設定
ルーティングの設定は次のようになります。
```ruby:routes.rb
  resources :messages, only: [:create]
  resources :rooms, only: [:create, :index, :show]
```


# コントローラーの作成
```
rails g controller rooms
```
```
rails g controller messages
```

#### users_controller
```ruby:users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user!

  def show
    @user = User.find_by(id: params[:id])
    # roomがcreateされた時に現在ログインしているユーザーと、
    # チャット相手になるユーザーの両方をEntriseテーブルから取得する。
    @current_entry = Entry.where(user_id: current_user.id)
    @another_entry = Entry.where(user_id: @user.id)
    unless @user.id == current_user.id
      @current_entry.each do |current|
        @another_entry.each do |another|
          if current.room_id == another.room_id
            @is_room = true
            @room_id = current.room_id
          end
        end
      end
      unless @is_room
        @room = Room.new
        @entry = Entry.new
      end
    end
    @posts = @user.posts.order(created_at: :desc).page(params[:page]).per(2)
    @like_posts = @user.likes.order(created_at: :desc).page(params[:page]).per(2)
    @repost_posts = @user.reposts.order(created_at: :desc).page(params[:page]).per(2)
    @comment_posts = @user.comments.order(created_at: :desc).page(params[:page]).per(3)
  end
```

#### ~解説~

```ruby
@current_entry = Entry.where(user_id: current_user.id)
@another_entry = Entry.where(user_id: @user.id)
```
ログインしているユーザーと、メッセージの相手のユーザーの情報をentryテーブルから検索して取得し、`@current_entry`と`@another_entry`に代入する。

```ruby
unless @user.id == current_user.id
      @current_entry.each do |current|
        @another_entry.each do |another|
          if current.room_id == another.room_id
            @is_room = true
            @room_id = current.room_id
          end
        end
      end
      unless @is_room
        @room = Room.new
        @entry = Entry.new
      end
    end
```
unless文でログインしていないユーザーという条件を設定します。
そしてさっき取得した2つのユーザー情報をそれぞれeachで取り出して、entryテーブル内に同じ`room_id`が存在するかどうかを調べます。

同じ`room_id`が存在する場合は既にroomがあるということなのでroom_idの変数とroomが存在するかどうかの条件であるis_roomを渡す。
同じroom_idが存在しない場合は新しくインスタンスを作成します。


#### 続いて、viewです。

```ruby:users/show.html.slim
.d-flex.justify-content-end.mt-4.me-4
      .me-2
        - unless @user.id == current_user.id 
          - if @is_room == true 
            = link_to  room_path(@room_id) do 
              i.fa-regular.fa-envelope.message
            
          - else 
            = form_for @room do |f|
              = fields_for @entry do |e|
                = e.number_field :user_id, value: @user.id, hidden: true
              
              = f.submit "メッセージを送る", class: "message_link"
```
![2024-03-13 18.27のイメージ (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/996b9e05-b2c9-0aab-0561-612a6cbe4bb4.jpeg)
![2024-03-13 18.31のイメージ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/e6b5a32f-1e28-e878-0b2f-a959171c67ca.jpeg)

ここでは、ログインしているユーザーではないという条件をつけてis_roomの条件を使用して既にroomが存在しているかどうかで分岐させる。
既にroomがあればそのroomに、なければformでパラメータを送っている。


#### rooms_controller
```ruby:rooms_controller.rb
class RoomsController < ApplicationController
  before_action :authenticate_user!

  def create
    @room = Room.create
    @current_entry = @room.entries.create(user_id: current_user.id)
    @another_entry = @room.entries.create(user_id: params[:entry][:user_id])
    redirect_to room_path(@room)
  end

  def index
    my_room_id = current_user.entries.pluck(:room_id)
    @another_entries = Entry
                       .where(room_id: my_room_id)
                       .where.not(user_id: current_user.id)
                       .preload(room: :messages).preload(user: { icon_attachment: :blob })
  end

  def show
    @room = Room.find(params[:id])
    if @room.entries.where(user_id: current_user.id).present?
      @messages = @room.messages.all
      @message = Message.new
      @entries = @room.entries
      @another_entry = @entries.where.not(user_id: current_user.id).first
    else
      redirect_back(fallback_location: root_path)
    end
  end
end

```

#### ~解説~
```ruby
 def create
    @room = Room.create
    @current_entry = @room.entries.create(user_id: current_user.id)
    @another_entry = @room.entries.create(user_id: params[:entry][:user_id])
    redirect_to room_path(@room)
  end
```

ここでusers/show.html.slimで部屋が存在しなかった場合にformで送られてきたパラメーターを受け取って、createさせます。
現在ログインしているユーザーに対しては、`@current_entry`として、entriesテーブルにRoom.createで作成された`@room`に紐づくidと、現在ログインしているユーザーのidを保存させる記述をする。

`@another_entry`では、メッセージの相手の情報をEntriesテーブルに保存するための記述。
users/show.html.slimのfields_for `@entry`で保存したparamsの情報(:user_id, :room_id)を許可し、現在ログインしているユーザーと同じく`@room`にひもづくidを保存する記述をしている。
最後にチャットルームが開くようにredirectをしている。


```ruby
def index
    my_room_id = current_user.entries.pluck(:room_id)
    @another_entries = Entry
                       .where(room_id: my_room_id)
                       .where.not(user_id: current_user.id)
                       .preload(room: :messages)
                       .preload(user: { icon_attachment: :blob })
  end
```
続いてindex
indexでは現在やり取りしていて存在しているroomの一覧を取得する。

まず最初にログイン中のユーザーがやり取りをしている全てのチャットルームのIDを取得する。
`current_user.entries.pluck(:room_id)`で現在ログインしているユーザーが参加しているentriesテーブルから、`room_id`カラムだけを抜き出して配列として取得する。

次に、最初に取得したroom_idに参加しているが、ログイン中のユーザーでない相手のエントリー情報を取得する。


#### 続いてindexのview

```ruby:rooms/index.html.slim
- @another_entries.each do |entry|
        = link_to room_path(entry.room), style: "text-decoration: none;" do
          .card
            .card-body
              div
                .d-flex
                  .chat-avatar
                    object
                      = link_to user_path(entry.user) do
                        = image_tag entry.user.icon, class: "message_icon_img me-3"
                  .chat-user-name
                    span.fw-bold.me-2
                      = entry.user.name
                    span.username
                      | @
                      = entry.user.username
                    .chat-text
                      = entry.room.messages.last&.message

```

![スクリーンショット 2024-03-13 19.03.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/e700c74f-978c-6997-6989-a91eb6054b7b.png)

```ruby
def show
  @room = Room.find(params[:id])
  if @room.entries.where(user_id: current_user.id).present?
    @messages = @room.messages.all
    @message = Message.new
    @entries = @room.entries
    @another_entry = @entries.where.not(user_id: current_user.id).first
  else
    redirect_back(fallback_location: root_path)
  end
end
```

showアクションでは、`@room`で1つのroomを表示させる。
`@messages`で過去のメッセージを全て表示させ、`@message`で新しいメッセージを作るためのインスタンスを用意している。
`@entries`と`@another_entry`でview側で相手の名前を表示させられるようにしている。

#### showのview
```ruby:rooms/show.html.slim
.col-md-12.border_right.border_left.d-flex.flex-column.justify-content-between.vh-100
  .d-flex.align-items-center.py-2
    = link_to root_path, class: "text-dark" do
      i.fa-solid.fa-arrow-left.arrow-left
    .profile_top_name
      h3
        = @another_entry.user.name
        | さんとのメッセージ

  .chat-body.p-3.flex-grow-1.overflow-auto
    - @messages.each do |m|
      - if m.user_id == current_user.id
        .d-flex.justify-content-end.mb-3
          .mycomment.p-2.rounded.d-inline-block
            = m.message
      - else
        .d-flex.justify-content-start.mb-3
          .fukidasi.d-flex
            .faceicon
              = image_tag m.user.icon, class: "message_icon_img mr-2"
            .chatting.p-2.rounded.bg-light.d-inline-block
              .says
                = m.message

  .chat-form-box.p-3
    = form_for @message do |f|
      .chat-form-group.d-flex
        = f.text_field :message, class: "form-control mr-2 flex-grow-1"
        = f.number_field :room_id, value: @room.id, hidden: true
        = f.submit "送信", class: "btn message_submit"
```

`@another_entry`で相手の名前などを表示させることができる。

メッセージの部分をif文を用いて分岐させることによって、DMっぽく自分のメッセージは右側、相手のメッセージは左側のようにスタイリングすることができます。

メッセージを送る際のform部分では、@messageがどこのroomに所属しているかを判断するために、room_idの情報を持たせている。
そしてこのformで送られたパラメータがmessage_controllerに渡っていきます。


#### messages_controller
```ruby:messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!

  def create
    message = current_user.messages.build(message_params)
    if message.save
      redirect_to room_path(message.room)
    else
      redirect_back(fallback_location: root_path)
    end
  end

  private

  def message_params
    params.require(:message).permit(:message, :room_id)
  end
end

```

rooms/show.html.slimで送られてきた、form_forのパラメータを実際に保存させるための記述をしています。
form_forで送られてきたmessageを含む全てのメッセージの情報の:messageと:room_idのキーがちゃんと入っているかということを条件で確認する。
もしその条件がtrueだったら、メッセージを保存するためにMessage.buildをし、Messagesテーブルに:message、room_idのパラメーターとして送られてきた値を許可する。

![スクリーンショット 2024-03-13 22.36.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/966dabec-a747-c452-92c6-75d9f9d90d6c.png)

# 最後に
こんな感じで、実装することができました。
リレーションも複雑で、なかなか理解を深めるのは初心者の私にはとても難しかったです。今でも完璧ではないですが、データの流れなどは実装していくとともに理解できたかなとは思います。
理解が薄い部分はもっと調べて理解を深めていきたいと思います！