hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

メッセージ送信機能実装

メッセージ送信機能実装のステップ

  1. モデルを作成する
  2. ルーティングを設定する
  3. 該当するアクションをコントローラに定義する
  4. メッセージ送信機能を実装する
  5. グループにメッセージを表示する
  6. サイドバーに最新のメッセージを表示する
  7. ヘッダーを修正する
  8. グループ編集ページへのリンクを設置する
  9. グループ編集後のリダイレクト先を変更する

1. モデルを作成する

メッセージ送信にあたって必要なモデルを作成。
このときにデータベース設計に従って、テーブルも作成しておく。

 問題1:Messageモデルを作成する

ターミナル
1
$ rails g model message
201XXXXXXXXXXX_create_messages.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.string :content
      t.string :image
      t.references :group, foreign_key: true
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end
ターミナル
1
$ rails db:migrate 
app/models/message.rb
1
2
3
4
5
6
class Message < ApplicationRecord
  belongs_to :group
  belongs_to :user

  validates :content, presence: true, unless: :image?
end
app/models/group.rb
1
2
3
4
5
6
class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
  has_many :messages
  validates :name, presence: true, uniqueness: true
end
app/models/user.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :group_users
  has_many :groups, through: :group_users
  has_many :messages
end

解説

マイグレーションファイルの設定について

group_idとuser_idはreferences型で設定してあるので、カラム名に_idは不要。
また、その2つのカラムには外部キー制約をつける。
なお、referencesを使用するとインデックスの設定も自動的に行われる。

バリデーションの設定について

メッセージモデルで、以下のバリデーションを設定している。

app/models/message.rb
1
2
3
4
5
class Message < ApplicationRecord
  belongs_to :group
  belongs_to :user
  validates :content, presence: true, unless: :image?
end

前半のvalidates :content, presence: trueは、contentカラムが空の場合は保存しない、というバリデーションである。

後半で、unless: :image?という条件を追加している。unlessはifの逆の役割がある。if: :image?であれば、imageカラムが空でなければという意味になるので、unless: :image?はimageカラムが空だったらという意味。

つまり、imageカラムが空の場合、contentカラムも空であれば保存しないという意味になる。

2. ルーティングを設定する

本番のChatSpaceのURLからどのようなルーティングが設定されているかを推測して、ルーティングを設定する。
メッセージのルーティングはグループにネストした形になっているので、意識してルーティングを設定する。

 問題2:ルーティングを設定する

routes.rb
1
2
3
4
5
6
7
8
Rails.application.routes.draw do
  devise_for :users
  root 'groups#index'
  resources :users, only: [:edit, :update]
  resources :groups, only: [:new, :create, :edit, :update] do
    resources :messages, only: [:index, :create]
  end
end

メッセージ送信機能の実装に必要なルーティングは、以下の通り。

  • 投稿されたメッセージの一覧表示 & メッセージの入力ができる:index
  • メッセージの保存を行う:create

 

3. 該当するアクションをコントローラに定義する

2で設定したルーティングで必要なアクションをコントローラに定義する。
まずは、大枠を作成してから、具体的な処理はこのあとで記述していくと良い。

4. メッセージ送信機能を実装する

メッセージは文章と画像を送信できるようにしておく。
画像の送信には、CarrierWaveというgemを使用。
投稿される画像は、アプリケーションのpublic以下に溜まっていく。
この画像はGitHubで管理する必要が無いものなので、.gitignoreに記述を追加してGitで管理しないようにする。
メッセージ送信後のリダイレクト先もきちんと指定する。

※幾つかの記事でcarrierwaveと共に「rmagick」の導入が促されているが、こちらのgemは最新版がインストール出来ない不具合があるためインストールしないようにする。かわりに「mini_magick」というgemをインストールすることをおすすめ。

 問題3:Carrierwaveを導入する

ターミナル
1
$ brew install imagemagick
Gemfile
1
2
3
# 〜省略〜
gem 'carrierwave'
gem 'mini_magick'
ターミナル
1
$ bundle install
ターミナル
1
$ rails g uploader image
app/models/message.rb
1
2
3
4
5
6
7
8
class Message < ApplicationRecord
  belongs_to :group
  belongs_to :user

  validates :content, presence: true, unless: :image?

  mount_uploader :image, ImageUploader
end
app/uploaders/image_uploader.rb
1
2
3
4
5
6
7
8
9
class ImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick
  # storage :fog
# 〜省略〜
  process resize_to_fit: [800, 800]
# 〜省略〜
end

解説

まずは必要なgemを導入。「carrierwave 」と「minimagick」をGemfileに追記し、ターミナルからbundle installを実行。

続いて、画像のアップローダーを作成する。ターミナルで、rails g uploader imageコマンドを実行すると、app/uploadersディレクトリ以下にimage_uploader.rbが作成される。

ターミナル
1
2
$ rails g uploader image
      create  app/uploaders/image_uploader.rb

アップローダーを作成したら、Messageモデルを編集しimage_uploaderをマウントする記述を行う。

app/models/message.rb
1
2
# 〜省略〜
  mount_uploader :image, ImageUploader

最後に、image_uploader.rbを編集して、MiniMagick経由で画像のリサイズを行えるようにする。
5行目に記述されている「include CarrierWave::MiniMagick」のコメントアウトを外す。

その後、任意の箇所に「process resize_to_fit: [800, 800]」と追記。resize_to_fitは縦横比を維持したまま、縦横を800px以内にリサイズするという意味。

これで、carrierwaveを用いて画像をアップロードする準備ができた。

 

フラッシュメッセージについて

メッセージにバリデーションをかけることで、文章も画像もない場合にはメッセージが保存されないようにする。
保存に失敗した際には、フラッシュメッセージを出し、ユーザーに保存できなかったことを伝える。

保存失敗時のフラッシュメッセージ
https://tech-master.s3.amazonaws.com/uploads/curriculums//621b91b05c3ef9c8c0a7d9b3470adb74.png

 問題4:メッセージ送信機能を実装する

messages_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MessagesController < ApplicationController
  before_action :set_group

  def index
    @message = Message.new
    @messages = @group.messages.includes(:user)
  end

  def create
    @message = @group.messages.new(message_params)
    if @message.save
      redirect_to group_messages_path(@group), notice: 'メッセージが送信されました'
    else
      @messages = @group.messages.includes(:user)
      flash.now[:alert] = 'メッセージを入力してください。'
      render :index
    end
  end

  private

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

  def set_group
    @group = Group.find(params[:group_id])
  end
end
app/views/shared/_main_chat.html.haml
1
2
3
4
5
6
7
8
9
# 〜省略〜
    .form
      = form_for [@group, @message] do |f|
        = f.text_field :content, class: 'form__message', placeholder: 'type a message'
        .form__mask
          = f.label :image, class: 'form__mask__image' do
            = icon('fas', 'image', class: 'icon')
            = f.file_field :image, class: 'hidden'
        = f.submit 'Send', class: 'form__submit'

解説

アクションの定義

まずは、messagesコントローラの記述を行う。必要となるアクションはindexcreateの2つ。

7つのアクション

アクション名 役割
index リソースの一覧を表示する
new リソースを新規作成する
create リソースを新規作成して追加(保存)する
edit リソースを更新するためのフォームを作成する
show レコードの内容を表示する
update リソースを更新する
destroy リソースを削除する

 

Railsでは、7つのアクションと呼ばれるルールに従って、コーディングを行う。

今回実装したい機能は以下の2つ。

  • メッセージの一覧を表示させたい
  • メッセージを追加したい

それぞれの機能に該当するアクションは、indexとcreate。

このように、Railsではあらかじめ決められたルールに従って、適切なアクションにコードを記述することが重要。これによって、初めてコードを見た開発者もだいたいどんなことをしようとしているアクションなのか推測しやすくなっている。

コントローラーの実装

private以下にset_groupを定義し、before_actionを利用して呼び出すことで、messagesコントローラの全てのアクションで@groupを利用できるようになる。

 

indexアクション

indexアクションでは、Messageモデルの新しいインスタンスである@message、グループに所属する全てのメッセージである@messagesを定義している。「n + 1 問題」を避けるために、includes(:user)の記述を忘れずに行う。

createアクション

createアクションでは、グループ作成機能を実装した際と同様に、保存に成功した場合保存に失敗した場合で処理を分岐させる。

ビューの実装

続いて、メッセージを保存できるように、ビューを編集する。formタグ、inputタグを用いて記述されている箇所をform_forを使って書き換える。

app/views/shared/_main_chat.html.haml
1
2
3
4
5
6
7
8
9
# 〜省略〜
    .form
      = form_for [@group, @message] do |f|
        = f.text_field :content, class: 'form__message', placeholder: 'type a message'
        .form__mask
          = f.label :image, class: 'form__mask__image' do
            = icon('fas', 'image', class: 'icon')
            = f.file_field :image, class: 'hidden'
        = f.submit 'Send', class: 'form__submit'

form_forの使い方

form_forの引数の意味

最初に、form_forの引数の意味を確認。

例えばform_for @groupという記述があるとする。この時の引数「@group」は、投稿されたデータをどこに送信すればいいのかを指定するために設定する。

@groupのなかにGroupモデルのインスタンスが入っている場合、Railsはそのクラス名から送信先を推定する。Groupモデルを保存するなら、次はGroupsコントローラーで処理を行うだろう、といった具合である。

ネストした場合のform_forの引数

しかし、今回のコードでは、form_forの引数に@group, @messageの2つを渡している点に注意。

これは、messagesのルーティングがgroupsにネストされているため。ChatSpaceでは、あるグループに属しているメッセージ、という親子関係がある。

そのため、form_forの第1引数@groupにはどのグループのメッセージとして保存したいのか、第2引数@messageにはMessageモデルのからのインスタンス(Message.new)をあらかじめセットしておく必要がある。

5. グループにメッセージを表示する

メッセージを投稿できるようになったら、投稿されたメッセージをビューに表示するようにする。

 問題5:投稿されたメッセージをビューに表示させる

app/views/shared/_main_chat.html.haml
1
2
3
4
5
6
-# (省略)

.messages
  = render @messages

-# (省略)
app/views/messages/_message.html.haml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.message
  .upper-message
    .upper-message__user-name
      = message.user.name
    .upper-message__date
      = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  .lower-message
    - if message.content.present?
      %p.lower-message__content
        = message.content
    = image_tag message.image.url, class: 'lower-message__image' if message.image.present?
views/shared/_side_bar.html.haml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.side-bar
  .header
    %h3.header__name
      = current_user.name
    %ul.header__lists
      %li.list
        = link_to new_group_path do
          = icon('fas', 'edit', class: 'icon')
      %li.list
        = link_to edit_user_path(current_user) do
          = icon('fas', 'cog', class: 'icon')
  .groups
    - current_user.groups.each do |group|
      .group
        = link_to group_messages_path(group) do
          .group__name
            = group.name
          .group__message
            メッセージはまだありません

解説

上記の解答では、render @messagesという記述で部分テンプレートを呼び出している。これは、以下のコードを省略した書き方。インスタンス変数の名前を単数形にしたものと、部分テンプレートの名前が同じならこのような省略をすることができる。

app/views/shared/_main_chat.html.haml
1
2
.messages
  = render partial: 'message', collection: @messages

また、以下の書き方で部分テンプレートを呼び出せた。

1
2
3
.messages
  - @messages.each do |message|
    = render partial: "message", locals: { message: message }

この書き方でも表示される結果は同じだが、処理スピードが遅いため、これからは使用しないように

https://tech-master.s3.amazonaws.com/uploads/curriculums//5db2879b7e2271d30b566e8ae32b57dc.png

each文を使って部分テンプレートを呼び出す場合、上図の左側のようにビューを作成するための処理が何度も実行される。そして、それぞれの処理に対して、@messagesのデータが1つずつ渡される。

それに対してcollectionを使うと、@messagesのデータは一括でまとめて渡され、ビュー作成のメソッドは1回の実行で済む。

 

6. サイドバーに最新のメッセージを表示する

サイドバーのグループ部分に最新のメッセージが表示されるように実装する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//a7c494220bcce9006fdd2ef4e7e4e16d.png

 問題6: サイドバーのグループ部分に最新のメッセージが表示されるようにする。またメッセージがないときは「まだメッセージはありません。」と表示されるようにする。

app/models/group.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Group < ApplicationRecord
  has_many :group_users
  has_many :messages
  has_many :users, through: :group_users

  validates :name, presence: true

  def show_last_message
    if (last_message = messages.last).present?
      if last_message.content?
        last_message.content
      else
        '画像が投稿されています'
      end
    else
      'まだメッセージはありません。'
    end
  end

end
views/shared/_side_bar.html.haml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.side-bar
  .header
    %h3.header__name
      = current_user.name
    %ul.header__lists
      %li.list
        = link_to new_group_path do
          = icon('fas', 'edit', class: 'icon')
      %li.list
        = link_to edit_user_path(current_user) do
          = icon('fas', 'cog', class: 'icon')
  .groups
    - current_user.groups.each do |group|
      .group
        = link_to group_messages_path(group) do
          .group__name
            = group.name
          .group__message
            = group.show_last_message
 こちらはあくまで模範解答。

解説

サイドバーのグループ部分に、最新のメッセージを表示できるように実装する。

最新のメッセージについて、文章が投稿されている場合、画像が投稿されている場合、まだ投稿がされていない場合が考えられる。かといって、ビュー部分にif、while、caseなどを用いて条件分岐を書いてしまうと、ビューのコードが複雑になってしまい、読みづらくなってしまう。

このような場合には、モデルにインスタンスメソッドを定義することで、ビューの記述をシンプルにすることができる。今回はapp/models/group.rbインスタンスメソッドを定義してみる。

app/models/group.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Group < ApplicationRecord
# 〜省略〜

  def show_last_message
    if (last_message = messages.last).present?
      if last_message.content?
        last_message.content
      else
        '画像が投稿されています'
      end
    else
      'まだメッセージはありません。'
    end
  end

end

show_last_messageメソッドでは、メッセージが投稿されている場合されていない場合で処理を分けている。5行目で、「if (last_message = messages.last).present?」と記述することで、最新のメッセージを変数last_messageに代入しつつ、メッセージが投稿されているかどうかで場合分けを行なっている。

メッセージが投稿されている場合の内部で、さらに文章が投稿されている場合画像が投稿されている場合で処理を分けている。

 

7. ヘッダーにグループ名とユーザー名が表示されるようにする

ヘッダーのグループ名も当該グループの名前が表示されるように、また、Member : sample_userの部分も、グループに所属しているユーザーの名前を表示できるようにする。
https://tech-master.s3.amazonaws.com/uploads/curriculums//d684ada00b44a5d227a48991644794bf.png

8. グループ編集ページへのリンクを設置する

「Edit」をクリックしたらチャットグループ編集に遷移するようにする。
https://tech-master.s3.amazonaws.com/uploads/curriculums//67f1ee75d1dd99114b4f4b6a08b29db4.gif

9. グループ編集後のリダイレクト先を変更する

今の仕様では、グループの情報を更新した後に、ルートパスへリダイレクトされるようになっている。

このリダイレクト先を変更して、今いるグループのメッセージ一覧が表示されるようにする。