実装する機能
グループの新規登録・編集機能を実装する。
「グループの新規登録」画面で、「グループ名」と「所属メンバー」を入力して登録できる機能を実装。所属メンバーは、アカウントを作成している全メンバーが表示されるようにする。
そのために、「グループ」と「ユーザー」の関連をデータベースでどのように管理するのかを考える。
実装の流れ
「登録・編集機能」と、「ビュー」を同時に扱うと手順がわかりにくくなるため、以下のステップで実装を進めていく。
①グループの新規登録機能を実装する
②グループの編集機能を実装する
③ビューのデザインを行う
グループの新規作成機能を実装
これから実装する機能を確認
グループの新規登録の際、どのような機能が必要になるか確認する。
ここでのゴールは、ユーザーが以下の操作をできるように機能を実装すること。
- テキストボックスにグループ名を入力する
- チェックボックスに全ユーザー名が表示され、所属させたいユーザーを選択することができる
- Saveボタンを押すと保存される
コントローラーを作成
グループを扱うためのコントローラーを作成する。以下の条件で作成する場合、どのようなコマンドを実行するのか確認して作成する。
問題1:グループのコントローラを作成をする
・コントローラー名は「groups」にする。
・新規作成機能の実装に必要なアクションを作成。ただし、アクションの中身は空で作成する。
・上記のアクションが呼ばれるよう、ルーティングの設定も合わせて行う。
下記のコマンドでgroupsコントローラを作成。
1 |
$ rails g controller groups
|
groupsコントローラーにアクションを追加。新規登録の時に必要な、「new」と「create」の定義をしておく。
1 2 3 4 5 6 7 8 9 |
class GroupsController < ApplicationController
def new
end
def create
end
end
|
最後にルーティングの設定を行う。
1 2 3 4 5 6 |
Rails.application.routes.draw do
devise_for :users
root 'messages#index'
resources :users, only: [:edit, :update]
resources :groups, only: [:new, :create]
end
|
ビューを変更してリンクを作成
ChatSpaceの仕様として、下記のアイコンをクリックするとグループの新規登録画面に移動するようになっている。
リンクで飛べるよう設定する
仕様通りになるよう、リンクの設定を行う。
①グループの新規作成画面を表示するビューファイルを追加する(中身は空で構わない)。
②リンクを設定し、アイコンをクリックしたら①のファイルが表示されるようにする。
モデルを作成
これから、データの保存に必要なモデルの作成を行う。ChatSpaceでは、新たなリレーションを使うため、最初にその確認をする。
「1対多」のリレーションの復習
「1対多」というリレーションはどのようなものだったか、簡単におさらい。
「1対多」のリレーションを使うと、「1つのツイートに対して、複数のコメントが書き込まれた」と行った関係性を表現することができる。
データベースでは、commentsテーブルの中に「tweet_id」カラムを作成し、その中に関連するtweetのidを保存することで関係を保存。
「多対多」のリレーション
それに対して、ChatSpaceの「グループ」と「ユーザー」は多対多のリレーションを組む。
1つのグループに複数のユーザーが所属していますし、逆に1人のユーザーは複数のグループに所属する。
つまり、お互いに1対多のような関係になっているため、「1対多」と同様の方法でテーブル設計を行うことができない。
これを解決するために「中間テーブル」を利用する。
group_usersテーブル(中間テーブル)に、「group_id」カラムと「user_id」カラムを作成。
あるユーザーがあるグループに所属していることを、group_idとuser_idを同じレコード(行)に登録することで表す。
モデルの作成を行う
「多対多」を利用したデータの保存を行う。まず必要なモデルファイル等を作成する。
多対多を実際に使用するためファイルの作成と編集を行なっていく。
まずはモデルファイルを作成。
groupモデルと、中間テーブルとして使用するgroup_userモデルを作成。
次にマイグレーションファイルの編集を行う。
1 2 3 4 5 6 7 8 9 |
class CreateGroups < ActiveRecord::Migration[5.2]
def change
create_table :groups do |t|
t.string :name, null: false
t.index :name, unique: true
t.timestamps
end
end
end
|
group_userモデルのマイグレーションファイルも編集を行う。
1 2 3 4 5 6 7 8 9 |
class CreateGroupUsers < ActiveRecord::Migration[5.2]
def change
create_table :group_users do |t|
t.references :group, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
end
end
|
マイグレートを実行。
1 |
$ rails db:migrate
|
ここまででテーブルの作成が完了。
多対多のアソシエーションを設定する
次にアソシエーションとバリデーションの設定を行う。
最初に、多対多のアソシエーションの設定を行う時の考え方を押さえておく。
アソシエーションを組むことで実現したいのは、groupsテーブルとusersテーブルを関連付けること。それによって、関係するデータを簡単に呼び出せるようにする。
そのため、上図のように「groupsテーブルと中間テーブル」および「usersテーブルと中間テーブル」の間に「1対多」のアソシエーションを組めば最低限の関連付けはできる。例えば「group.group_users.first.user」のような記述をすればデータの参照は不可能ではない。
しかし「group.users」のような書き方で、グループに所属するユーザーの情報を直接参照できる方が便利である。そのために「has_many :through関連付け」を行う。
アソシエーションを設定
以下のコードを記述。
1 2 3 4 5 |
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
validates :name, presence: true, uniqueness: true
end
|
この3行目に注目。
1 |
has_many :users, through: :group_users
|
has_many :through関連付け
「多対多」を使用する時によく使われる記述。
has_manyの引数に「アソシエーションを組みたいテーブル名」を、:throughのバリューに「中間テーブル名」を指定する。これによって、「group.users」といった呼び出し方ができるようになる。
他のテーブルについてもアソシエーションの設定を行う。
1 2 3 4 |
class GroupUser < ApplicationRecord
belongs_to :group
belongs_to :user
end
|
1 2 3 4 5 6 7 8 9 |
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :group_users
has_many :groups, through: :group_users
end
|
ユーザーを複数作成
これからグループに対して複数ユーザーの登録を行う。
もしも登録済みのユーザーが1人しかいない場合は、もう一名作成する。
データを保存してみる
ChatSpaceがあるディレクトリで、以下のコマンドを実行。
1 2 3 4 |
この操作によって、どのようなデータが作成されたがSequel Proで確認。
このように、groupsテーブルができると同時に、group_usersテーブルにもリレーションを表すレコードができていることがわかる。
1 |
$ Group.create(name: "グループ1", user_ids: [1, 2])
|
この部分の、「user_ids」というキー名がポイントです。「モデル名」+「_ids」というキーを使用すると、特別な意味を持つ。
groupという親要素を保存するときに、user_idsで所属させたいユーザーを配列で指定するとその情報が中間テーブルに保存される仕組みになっている。
ChatSpaceでもこの方法を利用して実装していく。
保存機能を実装
続いてチェックボックスを利用した、データ保存の機能を実装していく。
Railsには「collection_check_boxes」というヘルパーメソッドが用意されているので、これを利用する。
似たメソッドに「check_box」があるが、テーブルのデータと連携させる時は「collection_check_boxes」が便利である。
実装のステップを確認する
これからチェックボックスの使い方を確認しながら実装する。以下の3つのステップで進める。
① 新規登録画面にチェックボックスを表示させる
②フォームからどのようなデータが送信されるのか確認する
③投稿されたデータを保存する
①新規登録画面にチェックボックスを表示させる
最初にビューファイルにチェックボックスを表示させ、使い方を確認。
スタイルシートを適用しよう
今回使用するscssファイルはあらかじめ用意しています。以下の手順でアプリケーションに取り込む。
①scssファイルを「app/assets/stylesheets/modules」の下に作成。
②以下のように読み込みの設定を行う。
1 2 3 4 5 6 7 8 |
ビューファイルを編集
グループの新規投稿画面を以下の通り編集する。
1 2 3 4 |
=form_for @group do |f|
= f.text_field :name, class: "chat-group-form__input", placeholder: "グループ名を入力してください"
= f.collection_check_boxes :user_ids, User.all, :id, :name
= f.submit "Save", class: "chat-group-form__action-btn"
|
3行目のcollection_check_boxesについて使い方を確認。
collection_check_boxesの使い方
collection_check_boxesは、ビューにチェックボックスの表示を行うためのヘルパーメソッド 。
上記のように4つの引数をとる。
第2、第4引数は「フォームの表示」に使われる。
- 第2引数で、表示されるためのデータを指定する。
- 第4引数で、上記データのどのカラムを表示させるかを指定する。
第1、第3引数は、サブミットボタンが押された時に送信される「データの内容」を指定するもの。
- 第1引数は、送信されるハッシュの「キー」の名称。
- 第3引数は、送信されるハッシュの「バリュー」。
②フォームからどのようなデータが送信されるのか確認
次に、フォームから送信されたデータをコントローラーで保存できるようにする。
pryをインストール
次の手順で、アプリを一時停止して確認する。
事前にpry-railsをインストール。
1 2 3 |
(前略)
gem 'pry-rails'
(後略)
|
Gemfileに追記したら、bundle installとサーバーの再起動も忘れずに。
Groupsコントローラーを編集
1 2 3 4 5 6 7 8 9 10 11 12 |
class GroupsController < ApplicationController
def new
@group = Group.new
@group.users << current_user
end
def create
binding.pry
end
end
|
追加したコードの内容。
3 4 5 6 |
def new
@group = Group.new
@group.users << current_user
end
|
newアクションの中で、@groupに空のインスタンスを代入している。
これは、form_forで使用するための変数。
5行目で「<<」という記号が使われているが、これは配列に要素を追加するためのもの。
ChatSpaceの仕様として、グループを新規作成する時はログイン中のユーザーを必ず含めたいためあらかじめ追加しておく。
8 9 10 |
def create
binding.pry
end
|
createアクションはまだ定義を行っていない。
フォームから送信されるparamsを確認するため、binding.pryを追加しておく。
送信されるデータを確認する
ここまでのアプリを動かして、チェックボックスを利用するとどのようなデータが送信されるのか確認。
1 |
$ rails s
|
一度仮想サーバーを起動して、実際に投稿を行ってみる。
この時点では、デザインを行っていないため、レイアウトが崩れていて問題ない。
投稿するとbinding.pryによって動作が止まるので、「仮想サーバーを起動したターミナル」で送信されたデータを見てみる。
1 2 3 4 5 |
$ params
=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"PUudmtnrFqhPT80hPJBK8I
Un/Uq0Id7Zvm2qiKV2OguM/f2BPOGG0OCB6YocTEBpx3mQ1iUuRgm/Nee7vbOo8Q==", "group"=>{"name"=>"新規
グループ", "user_ids"=>["", "1", "3"]}, "commit"=>"Save", "controller"=>"groups", "action"=>
"create"} permitted: false>
|
このような表示がされますが、必要な部分に着目。
1 |
{"name"=>"新規グループ", "user_ids"=>["", "1", "3"]}
|
このように、「name」キーに「グループ名」が、「user_ids」キーに「所属ユーザーのidの配列」が送信されていることがわかる。
では、この配列を利用して中間テーブル保存する方法を確認。
③投稿されたデータを保存
このハッシュのデータを使えば以下のように実行することで、中間テーブルに保存を行うことができる。
1 |
$ Group.create(name: "グループ1", user_ids: [1, 2])
|
しかし、ここで問題になるのがストロングパラメータである。
Railsでは、保存する前に必ずストロングパラメータを使って許可を行わなければ、データの保存がされないようセキュリティ対策が取られている。
今回は、バリューが配列で送られてきているため、配列の保存を許可するためのストロングパラメータが必要になる。
Groupsコントローラーを編集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class GroupsController < ApplicationController
def new
@group = Group.new
@group.users << current_user
end
def create
@group = Group.new(group_params)
if @group.save
redirect_to root_path, notice: 'グループを作成しました'
else
render :new
end
end
private
def group_params
params.require(:group).permit(:name, user_ids: [])
end
end
|
この中で、ストロングパラメータの部分に注目。
25 |
params.require(:group).permit(:name, user_ids: [])
|
この中に、「user_ids: 」という記述がある。このように、配列に対して保存を許可したい場合は、キーの名称と関連づけてバリューに「」と記述する。
またcreateアクション内の記述も注意が必要なので確認しておく。
1 2 3 4 5 6 7 8 |
def create
@group = Group.new(group_params)
if @group.save
redirect_to root_path, notice: 'グループを作成しました'
else
render :new
end
end
|
ここでは、保存がうまくいったかどうかで処理を分岐している。
うまくいった場合はルートパスに遷移して、「グループを作成しました」とメッセージ表示をする。うまく行かなかった場合は新規登録画面を表示している。
ここで使用しているメソッドが、「redirect_to」と「render」でそれぞれ異なる。
redirect_toとrender
redirect_toとrenderのいずれも、実行するとビューが表示されます。
しかし、表示までの経路が異なる。
redirect_toの場合は、新たなリクエストがされたのと同じ動きになるので、コントローラーを経由してビューが表示される。
それに対してrenderの場合はそのままビューが表示される。
これによって、元のインスタンス変数の値が上書きされるかどうかが違う。
@groupという変数にはエラーメッセージが代入されているため、これが上書きされないようrenderによってビューを表示させる必要がある。
動作確認を行う
ここまで機能を実装できたら、実際にグループの新規作成を行う。
作成後、Sequel Proでデータが追加されていることを確認。
グループの編集機能を実装
ここまでで、グループの新規作成機能が実装できたので、続いて編集ができるように実装する。
編集機能を実装
編集機能の実装は、新規登録とほとんど同じ。
問題2:グループの編集機能を実装する
1 2 3 4 5 6 |
Rails.application.routes.draw do
devise_for :users
root 'messages#index'
resources :users, only: [:edit, :update]
resources :groups, only: [:new, :create, :edit, :update]
end
|
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 30 31 32 33 34 35 |
class GroupsController < ApplicationController
def new
@group = Group.new
@group.users << current_user
end
def create
@group = Group.new(group_params)
if @group.save
redirect_to root_path, notice: 'グループを作成しました'
else
render :new
end
end
def edit
@group = Group.find(params[:id])
end
def update
@group = Group.find(params[:id])
if @group.update(group_params)
redirect_to root_path, notice: 'グループを更新しました'
else
render :edit
end
end
private
def group_params
params.require(:group).permit(:name, user_ids: [] )
end
end
|
1 2 3 4 |
= form_for @group do |f|
= f.text_field :name, class: "chat-group-form__input", placeholder: "グループ名を入力してください"
= f.collection_check_boxes :user_ids, User.all, :id, :name
= f.submit "Save", class: "chat-group-form__action-btn"
|
※ ファイルを新規作成してから、上記のコードを記述する。
解説
form_forを使用しているため、新規登録と更新でコードはほとんど一緒。
なお、リンク設定を行っていないため、アプリからグループの編集画面に移動することができない。
そのため、動作の確認を行うときはURL欄に「localhost:3000/groups/1/edit」などと直接アドレスを入力する。
ビューのデザインを行う
最後に、ビューのデザインやリンク設定など、見た目の仕上げを行う。
パスの指定を変更
これまでは、ルートパスにアクセスがあったらmessagesコントローラーのindexアクションが動くよう仮で設定していた。
見本のChatSpaceと同じパスになるよう変更する。
ChatSpaceは、ルートパスにアクセスするとgroupsコントローラーのindexアクションが動く。
groups#indexはグループ一覧を表示表示するビューなので、左側のサイドバーだけが表示される。
messages#indexはメッセージ一覧を表示するビュー。サイドバーのグループ名をクリックすると、そのグループのメッセージ一覧が表示される。
routes.rbを編集
1 2 3 4 5 6 |
Rails.application.routes.draw do
devise_for :users
root 'groups#index'
resources :users, only: [:edit, :update]
resources :groups, only: [:new, :create, :edit, :update]
end
|
3行目でルートパスを変更している。
ビューの表示を整える
ここまでは、投稿などの機能を実装しビューのデザインが崩れていた。ビューでクラス指定を行ってCSSが適用されるようにする。
また、新規登録と編集のビューはほとんど同じため、部分テンプレートを活用して統一化する。
部分テンプレートを作成
「app/views/groups」フォルダに、「_form.html.haml」という名前でファイルを新規作成する。
そのファイルに以下のコードを記述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
= form_for group do |f|
.chat-group-form__errors
%h2 10件のエラーが発生しました
%ul
%li nameを入力してください
.chat-group-form__field
.chat-group-form__field--left
= f.label :name, class: 'chat-group-form__label'
.chat-group-form__field--right
= f.text_field :name, class: 'chat__group_name chat-group-form__input', placeholder: 'グループ名を入力してください'
.chat-group-form__field.clearfix
/ この部分はインクリメンタルサーチ(ユーザー追加の非同期化のときに使用します
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
.chat-group-form__field--right
/ グループ作成機能の追加時はここにcollection_check_boxesの記述を入れてください
= f.collection_check_boxes :user_ids, User.all, :id, :name
/ この部分はインクリメンタルサーチ(ユーザー追加の非同期化のときに使用します
.chat-group-form__field.clearfix
.chat-group-form__field--left
.chat-group-form__field--right
= f.submit class: 'chat-group-form__action-btn'
|
今後エラーメッセージの表示機能を実装するので、今は仮置きでメッセージが表示されるようになっている。
新規登録、編集時に、このフォームが読み込まれるようにする。
ビューファイルを編集
フォームの内容は、_form.html.hamlに移動させている。new.html.hamlとedit.html.hamlの中身を、全て削除してから以下のコードを記述。
1 2 3 |
.chat-group-form
%h1 新規チャットグループ
= render partial: 'form', locals: { group: @group }
|
1 2 3 |
.chat-group-form
%h1 チャットグループ編集
= render partial: 'form', locals: { group: @group }
|
newとeditからは、ほとんど_formを呼び出すだけになる。
タイトルは異なるので、それぞれのファイル内に記載する。
また、index.html.hamlを新しく以下のように作成。
1 2 |
.wrapper
= render 'shared/side_bar'
|
こちらもside_bar.html.hamlを呼び出すだけになる。
form_for、form_withの仕様
新規登録時も更新時も、全く同じフォームのコードを使用している。
しかし、サブミットボタンを押した後のデータの送信先は両者で異なるはず。データの保存はcreateアクションで、更新はupdateアクションで行う。
それにもかかわらず同じコードで正常に動くのはなぜなのか。
form_for/form_withメソッドは、引数の内容によってデータの送信先を推測している。
「form_for @group」という記述を行なった場合、以下の動作となります。form_withの場合も同様。
- @groupに空のデータが入っている → 送信時にcreateアクションが呼ばれる
- @groupに既存のデータが入っている → 送信時にupdateアクションが呼ばれる
トップページの表示を追加
トップページでは、ログイン中のユーザーが所属しているグループが一覧表示される。
サイドバーに、グループ名とそのグループの最新メッセージが表示されるよう実装する。
問題3:サイドバーにグループ一覧を表示する。また、ペンマークのアイコンをクリックしたら、グループの新規作成画面に遷移するよう設定する
1 2 3 4 5 6 |
Rails.application.routes.draw do
devise_for :users
root 'groups#index'
resources :users, only: [:edit, :update]
resources :groups, only: [:index, :new, :create, :edit, :update]
end
|
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 30 31 32 33 34 35 36 37 |
class GroupsController < ApplicationController
def index
end
def new
@group = Group.new
@group.users << current_user
end
def create
@group = Group.new(group_params)
if @group.save
redirect_to root_path, notice: 'グループを作成しました'
else
render :new
end
end
def edit
end
def update
@group = Group.find(params[:id])
if @group.update(group_params)
redirect_to root_path, notice: 'グループを更新しました'
else
render :edit
end
end
private
def group_params
params.require(:group).permit(:name, user_ids: [] )
end
end
|
クラス名の指定について
下記の「_side_bar.html.haml」のコードは解答例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.side-bar
.side-bar__user-name
.top-items
masa
.top-items__icons
= link_to new_group_path do
%i.fa.fa-pencil-square-o.header-items__icons--pen
= link_to edit_user_path(current_user.id) do
%i.fa.fa-cog.header-items__icons--cog
.side-bar__groups-list
- current_user.groups.each do |group|
.group
= link_to '#' do
.group__name
= group.name
.group__message
メッセージはまだありません。
|
解説
groupsコントローラー
groupsコントローラーにindexアクションを追加した。
今回はインスタンス変数の設定が不要なため、アクションの中身は何もない。
このような場合、アクションの定義はなくても動作しますが、indexアクションが存在することがわかりやすいよう必ず定義を記述しておく。
_side_bar.html.haml
ログイン中のユーザーが属しているグループをアソシエーションで取得し、一覧表示している。
メッセージの投稿はまだできないので、仮置きでテキストを表示しておく。
エラーメッセージを表示
最後に、グループの新規登録や更新で失敗した時は、エラーメッセージが表示されるようにする。
ビューファイルを変更する。
_form.html.hamlを変更
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 |
= form_for group do |f|
- if group.errors.any?
.chat-group-form__errors
%h2= "#{group.errors.full_messages.count}件のエラーが発生しました。"
%ul
- group.errors.full_messages.each do |message|
%li= message
.chat-group-form__field
.chat-group-form__field--left
= f.label :name, class: 'chat-group-form__label'
.chat-group-form__field--right
= f.text_field :name, class: 'chat__group_name chat-group-form__input', placeholder: 'グループ名を入力してください'
.chat-group-form__field.clearfix
/ この部分はインクリメンタルサーチ(ユーザー追加の非同期化のときに使用します
.chat-group-form__field.clearfix
.chat-group-form__field--left
%label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー
.chat-group-form__field--right
/ グループ作成機能の追加時はここにcollection_check_boxesの記述を入れてください
= f.collection_check_boxes :user_ids, User.all, :id, :name
/ この部分はインクリメンタルサーチ(ユーザー追加の非同期化のときに使用します
.chat-group-form__field.clearfix
.chat-group-form__field--left
.chat-group-form__field--right
= f.submit class: 'chat-group-form__action-btn'
|
ここでは、エラーメッセージ取得の方法を確認する。
errorsメソッド
group.saveのように変数groupの保存をしようとした時に、失敗するとgroupにエラーメッセージが格納される。
その変数に対してerrorメソッドを使用するとエラーメッセージの取得ができる。
上記のコードでは、さらに以下のメソッドを続けて使用している。
- any?メソッドによって、エラーメッセージの有無を判定している。
- full_messagesメソッドによってエラーメッセージ全件の内容を取得している。
ここまでで機能の実装は完了。動作確認とGitの操作を行う。
アプリの動作確認を行う
アプリを起動して、以下の機能が正常に動くことを確認。
- グループの新規登録ができること
- グループの編集ができること
- グループの登録、更新に失敗したらエラーメッセージが表示されること