Railsで作成した1対1のメッセージ機能をコードリーディング

## はじめに
Railsで1対1のメッセージ機能を実装したアプリをコードリーディングします。Qiita初投稿のプログラミング初心者です！温かい目で見てください涙

## 開発環境
ruby: 2.6.5
rails: 5.2.4.1
postgresqlを使用

## 設計
- テキスト形式で会話（メッセージのやり取り）ができる
- 会話を取っている相手間でのみ情報が公開される
- 会話を取っている相手間のメンバー毎に「会話ひとつ」が定義される

### 要件
- ユーザ一覧、あるいはユーザ個別の画面からメッセージ画面を表示できる
- メッセージ画面では相手と自分の名前が表示される
- メッセージ画面には個々のメッセージの発言者と内容が時系列で表示される
- メッセージ一覧リンクから過去に自分がやりとりしたメッセージすべてを見ることができる

### テーブルの仕様<img width="765" alt="スクリーンショット 2020-01-22 13.21.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276449/8eecc012-8a53-7298-c6a5-fc3264b249f0.png">

**このアプリはログインできるUser機能を持ったアプリケーションに機能を追加して行く形で実装して行きました。**

## コード内容

### route
```config/route.rb
Rails.application.routes.draw do
  # 省略
  resources :conversations do
    resources :messages
  end
end
```

### controller


```app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
  before_action :authenticate_user
  def index
    @conversations = Conversation.all
  end

  def create
    if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end
    redirect_to conversation_messages_path(@conversation)
  end

  private

  def conversation_params
    params.permit(:sender_id, :recipient_id)
  end
end
```
```app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action do
    @conversation = Conversation.find(params[:conversation_id])
  end

  def index
    @messages = @conversation.messages

    if @messages.length > 10
      @over_ten = true１
      @messages = Message.where(id: @messages[-10..-1].pluck(:id))
    end

    if params[:m]
      @over_ten = false
      @messages = @conversation.messages
    end

    if @messages.last
      @messages.where.not(user_id: current_user.id).update_all(read: true)
    end

    @messages = @messages.order(:created_at)
    @message = @conversation.messages.build
  end

  def create
    @message = @conversation.messages.build(message_params)
    if @message.save
      redirect_to conversation_messages_path(@conversation)
    else
      render 'index'
    end
  end

  private

  def message_params
    params.require(:message).permit(:body, :user_id)
  end
end
```
### model
```app/models/conversation.rb
class Conversation < ApplicationRecord
  belongs_to :sender, foreign_key: :sender_id, class_name: 'User'
  belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User'

  has_many :messages, dependent: :destroy

  validates_uniqueness_of :sender_id, scope: :recipient_id

  scope :between, -> (sender_id,recipient_id) do
    where("(conversations.sender_id = ? AND conversations.recipient_id =?) OR (conversations.sender_id = ? AND  conversations.recipient_id =?)", sender_id,recipient_id, recipient_id, sender_id)
  end

  def target_user(current_user)
    if sender_id == current_user.id
      User.find(recipient_id)
    elsif recipient_id == current_user.id
      User.find(sender_id)
    end
  end
end
```

```app/models/message.rb
class Message < ApplicationRecord
  belongs_to :conversation
  belongs_to :user

  validates_presence_of :body, :conversation_id, :user_id

  def message_time
    created_at.strftime("%m%d%y at %l:%M %p")
  end
end
```

### view

```app/views/conversations/index.html.erb
<table class="table table-hover">
  <thead>
    <h2>メッセージ一覧</h2>
  </thead>
  <tbody>
    <% @conversations.each do |conversation| %>
      <td>
        <% if conversation.target_user(current_user).present? %>
          <%= link_to conversation.target_user(current_user).name, conversation_messages_path(conversation)%>
        <% end %>
      </td>
    <% end %>
  </tbody>
</table>
```

```app/views/messages/index.html.erb
<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>

<div class="ui segment">
  <% @messages.each do |message| %>
    <% if message.body.present? %>
      <div class="item">
        <div class="content">
          <div class="header"><strong><%= message.user.name %></strong> <%= message.message_time %></div>
          <div class="list">
            <div class="item">
              <i class="right triangle icon"></i>
              <%= message.body %> /
              <% if message.user == current_user %>
                <%= message.read ? '既読' : '未読' %>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    <% end %>
  <% end %>
</div>

<%= form_with(model: [@conversation, @message]) do |f| %>
  <div class="field">
    <%= f.text_area :body, class: "form-control" %>
  </div>
    <%= f.text_field :user_id, value: current_user.id, type: "hidden"  %>
  <div>
    <%= f.submit "メッセージを送る" %>
  </div>
<% end %>
```

## コードリーディング
それではコードリーディングしてきます。
わかりにくかった部分を記載します。

### わからなかった部分のコードその１
```app/models/conversation.rb
scope :between, -> (sender_id,recipient_id) do
  where(["(conversations.sender_id = ? AND conversations.recipient_id = ?) OR (conversations.sender_id = ? AND conversations.recipient_id = ?)", sender_id ,recipient_id, recipient_id, sender_id])
end
```
```app/controllers/conversations_controller.rb
def create
  if logged_in?
    if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end
    redirect_to conversation_messages_path(@conversation)
  else  
    redirect_to root_path
  end 

# 省略
  private　　

  def conversation_params
    params.permit(:sender_id, :recipient_id)
  end 
end
```
### 下記のように理解しました。その1
まずログインしているか確認して(ログインしてなかったらルートパスへ)、

```ruby
if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end
```
このコードでConversationテーブルに、送り主(メッセージボタンを押した人、ログインユーザー)の idに```sender_id```があって、かつ、受け取り主(メッセージボタンを押された人)のidに```recipient_id```がある、もしくは受け取り主のidに```sender_id```があって、かつ、送り主のidに```recipient_id```がある(つまり、チャットルームがある)場合とそうでない場合で分ける。

チャットルームがある場合、
そのチャットルームに必要な情報が紐づいている、Conversationの１レコードの情報を`@conversation`に代入。
なぜfirstクエリメソッドを使っているかというと、Conversationに「```sender_id```に送り主のid、```recipient_id```に受け取り主のid」の組み合わせと「```sender_id```に受け取り主のid、```recipient_id```に送り主のid」の組み合わせの2つパターンどちらかが存在するから。（２つは同じチャットルーム)

チャットルームがない場合は、Conversationに新規で```sender_id```と```recipient_id```に送り主と送り主のidで作成し、`@conversation`に代入。

そして、`redirect_to conversation_messages_path(@conversation)`で`@conversation`とともにmessageのコントローラ、indexアクションに飛ぶ。


### わからなかった部分のコードその２
```app/controllers/messages_controller.rb
def index
  @messages = @conversation.messages


  if @messages.length > 10
    @over_ten = true
    @messages = Message.where(id: @messages[-10..-1].pluck(:id))
  end


  if params[:m]
    @over_ten = false
    @messages = @conversation.messages
  end


  if @messages.last
    @messages.where.not(user_id: current_user.id).update_all(read: true)
  end


  @messages = @messages.order(:created_at)
  @message = @conversation.messages.build
end
```
### 下記のように理解しました。その２
```ruby
@messages = @conversation.messages
```
`@messages`にconversations＃createから送られてきた`@conversation`(そのチャットルーム)に、関連された全メッセージを代入。

```ruby
if @messages.length > 10
    @over_ten = true
    @messages = Message.where(id: @messages[-10..-1].pluck(:id))
  end
```
もし、`@messages`が１０個より多かった場合、
10より大きいというフラグを有効にする。
`@messages[-10..-1]`で１０件のみメッセージ情報を取得、しかしこの書き方だとArrayクラスになり、whereクエリメソッドなどが使えないため、pluckで一度配列でidのみを受けり、その配列をもとにwhereで検索して`@messages`に代入。ちなみに`messages[-10..-1]`は後ろから１0番目と後ろから１番目の間のメッセージ情報という意味。

```ruby
if @messages.last
    @messages.where.not(user_id: current_user.id).update_all(read: true)
  end
```
最新（最後）のメッセージが存在している場合、ログインユーザー以外のメッセージを全て抽出して、readカラムをtrueにする(既読にする)。ログインユーザーは`read: false`のままで、チャット相手がログインしてmessages#indexに飛んでくれないと`read: true`にならない。

```ruby
@message = @conversation.messages.build
```
そのチャットルームに関連された、messageのインスタンスを生成。

### わからなかった部分のコードその３
```app/views/messages/index.html.erb
<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>

<div class="ui segment">
  <% @messages.each do |message| %>
    <% if message.body.present? %>
      <div class="item">
        <div class="content">
          <div class="header"><strong><%= message.user.name %></strong> <%= message.message_time %></div>
          <div class="list">
            <div class="item">
              <i class="right triangle icon"></i>
              <%= message.body %> /
              <% if message.user == current_user %>
                <%= message.read ? '既読' : '未読' %>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    <% end %>
  <% end %>
</div>

<%= form_with(model: [@conversation, @message]) do |f| %>
  <div class="field">
    <%= f.text_area :body, class: "form-control" %>
  </div>
    <%= f.text_field :user_id, value: current_user.id, type: "hidden"  %>
  <div>
    <%= f.submit "メッセージを送る" %>
  </div>
<% end %>
```
### 下記のように理解しました。その３
```
<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>
```
このメッセージ数が１０個以上の場合、以前のメッセージのリンクが表示され、クリックすると/conversations/:id/messages?m=allが送られ、

```app/controllers/messages_controller.rb
if params[:m]
    @over_ten = false
    @messages = @conversation.messages
  end
```
の処理が実行される。なお、allの部分は任意の文字でよくて、とにかくmに変数を入れてあげればいい。

```app/model/conversation.rb
def target_user(current_user)
    if sender_id == current_user.id
      User.find(recipient_id)
    elsif recipient_id == current_user.id
      User.find(sender_id)
    end
  end
```
ログインユーザーに関連している他のユーザーの情報を取得するメソッド。
ログインユーザーがチャットルームを作成したパターンと、ログインユーザーの相手がチャットルームを作成したパターンのどちらかで存在するから、どちらにも対応できるようになっている。

## バリデーションに引っかからない、エラーを吐かない場合の全体の流れ　
user/index
↓メッセージボタン
conversations_controller(create)
↓redirect_to
massage_controller(index)
↓
massages/index ※
↓メッセージを送るボタン
massages_controller(create)
↓
※に行く

以上です。