メッセージ送信機能実装
メッセージ送信機能実装のステップ
- モデルを作成する
- ルーティングを設定する
- 該当するアクションをコントローラに定義する
- メッセージ送信機能を実装する
- グループにメッセージを表示する
- サイドバーに最新のメッセージを表示する
- ヘッダーを修正する
- グループ編集ページへのリンクを設置する
- グループ編集後のリダイレクト先を変更する
1. モデルを作成する
メッセージ送信にあたって必要なモデルを作成。
このときにデータベース設計に従って、テーブルも作成しておく。
問題1:Messageモデルを作成する
1 |
$ rails g model message
|
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
|
1 2 3 4 5 6 |
class Message < ApplicationRecord
belongs_to :group
belongs_to :user
validates :content, presence: true, unless: :image?
end
|
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
|
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を使用するとインデックスの設定も自動的に行われる。
バリデーションの設定について
メッセージモデルで、以下のバリデーションを設定している。
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:ルーティングを設定する
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
|
1 2 3 |
# 〜省略〜
gem 'carrierwave'
gem 'mini_magick'
|
1 |
$ bundle install
|
1 |
$ rails g uploader image
|
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
|
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をマウントする記述を行う。
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を用いて画像をアップロードする準備ができた。
フラッシュメッセージについて
メッセージにバリデーションをかけることで、文章も画像もない場合にはメッセージが保存されないようにする。
保存に失敗した際には、フラッシュメッセージを出し、ユーザーに保存できなかったことを伝える。
保存失敗時のフラッシュメッセージ
問題4:メッセージ送信機能を実装する
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
|
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コントローラの記述を行う。必要となるアクションはindexとcreateの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を使って書き換える。
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:投稿されたメッセージをビューに表示させる
1 2 3 4 5 6 |
-# (省略)
.messages
= render @messages
-# (省略)
|
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?
|
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
という記述で部分テンプレートを呼び出している。これは、以下のコードを省略した書き方。インスタンス変数の名前を単数形にしたものと、部分テンプレートの名前が同じならこのような省略をすることができる。
1 2 |
.messages
= render partial: 'message', collection: @messages
|
また、以下の書き方で部分テンプレートを呼び出せた。
1 2 3 |
.messages
- @messages.each do |message|
= render partial: "message", locals: { message: message }
|
この書き方でも表示される結果は同じだが、処理スピードが遅いため、これからは使用しないように
。
each文を使って部分テンプレートを呼び出す場合、上図の左側のようにビューを作成するための処理が何度も実行される。そして、それぞれの処理に対して、@messagesのデータが1つずつ渡される。
それに対してcollectionを使うと、@messagesのデータは一括でまとめて渡され、ビュー作成のメソッドは1回の実行で済む。
6. サイドバーに最新のメッセージを表示する
サイドバーのグループ部分に最新のメッセージが表示されるように実装する。
問題6: サイドバーのグループ部分に最新のメッセージが表示されるようにする。またメッセージがないときは「まだメッセージはありません。」と表示されるようにする。
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
|
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
にインスタンスメソッドを定義してみる。
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
の部分も、グループに所属しているユーザーの名前を表示できるようにする。
8. グループ編集ページへのリンクを設置する
「Edit」をクリックしたらチャットグループ編集に遷移するようにする。
9. グループ編集後のリダイレクト先を変更する
今の仕様では、グループの情報を更新した後に、ルートパスへリダイレクトされるようになっている。
このリダイレクト先を変更して、今いるグループのメッセージ一覧が表示されるようにする。