Rails Controller Fundamentals
— とっとるびー第31回 2018-09-24
— https://tottoruby.connpass.com/event/100646/
— @hamajyotan (SAKAGUCHI Takashi)
さきにお伝え
— ちょっとした Rails コードを動かしたりします. 以下
の状態が手元にあると, 一緒に試したりできます.
— Ruby 2.5.1 , Rails 5.2.1 で実施しますが, そこまで
厳密に合わせる必要はないです.
— gem sqlite3. (rails new で database を指定しない
ので既定で使っているだけであり, 行間読んで別
DB を指定できればそれで OK です)
自己紹介
— SAKAGUCHI Takashi
— Office UMMM LLC
— Ruby とか Rails とかがちょっとできるおじさんです
今日話すこと
— scaffold よくみてみようという件
— resource routing にこだわろうという件
— template inheritance 便利だよという件
scaffold よくみてみようという件
簡単なアプリを作りつつ
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
$ rails -v
Rails 5.2.1
$ rails new example --skip-active-storage
出力省略...
$ cd example/
$ ./bin/rails s
➡ http://localhost:3000
簡単なアプリを作りつつ
$ ./bin/rails scaffold message 
title body:text 
published:boolean
$ ./bin/rails db:migrate
➡ http://localhost:3000/
messages
resource(s) なルーティング
config/routes.rb
Rails.application.routes.draw do
resources :messages
end
$ rails routes
Prefix Verb URI Pattern Controller#Action
messages GET /messages(.:format) messages#index
POST /messages(.:format) messages#create
new_message GET /messages/new(.:format) messages#new
edit_message GET /messages/:id/edit(.:format) messages#edit
message GET /messages/:id(.:format) messages#show
PATCH /messages/:id(.:format) messages#update
PUT /messages/:id(.:format) messages#update
DELETE /messages/:id(.:format) messages#destroy
アクションとその意味
Action 意味
index 一覧表示(画面)
create 新規作成
new 新規作成(画面)
edit 編集(画面)
show 詳細表示(画面)
update 更新
destroy 削除
html 都合の new/edit はこのさい置いとく
Resource HTTP Method Action 意味
/messages GET index 一覧表示(画面)
/messages POST create 新規作成
/message/:id GET show 詳細表示(画面)
/message/:id PATCH/PUT update 更新
/message/:id DELETE destroy 削除
HTTP のメソッド
HTTP Method /messages /message/:id
GET index show
PUT/PATCH - update
DELETE - destroy
POST create -
HTTP のメソッド
HTTP Method 意図など 安全 冪等
GET リソースの取得 ! !
PUT/PATCH リソースの配置 !❓
DELETE リソースの削除 !❓
POST リソースの生成
GET
— リソースの取得. 常に安全で冪等.
— scaffold の実装だと, コレクションへの GET は常に
200 でメンバーへの GET はない場合 404 を返す.
def index
@messages = Message.all
end
def show
end
PUT/PATCH
— リソースの配置.
— scaffold の実装の場合存在しない場合は 404 を返す
が, 定義からすれば ID を明示した merge (insert or
update) とした方が適当かも.
def update
respond_to do |format|
if @message.update(message_params)
format.html { redirect_to @message, notice: 'Message was successfully updated.' }
format.json { render :show, status: :ok, location: @message }
else
format.html { render :edit }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
DELETE
— リソースの削除.
— scaffold の実装の場合存在しない場合は 404 を返す
が, 定義からすれば存在しない場合は何もしないのが
適当かも.
def destroy
@message.destroy
respond_to do |format|
format.html { redirect_to messages_url, notice: 'Message was successfully destroyed.' }
format.json { head :no_content }
end
end
POST
— リソースの生成
— 「生成」としているのはコレクションリソースに対
する操作であるため.
def create
@message = Message.new(message_params)
respond_to do |format|
if @message.save
format.html { redirect_to @message, notice: 'Message was successfully created.' }
format.json { render :show, status: :created, location: @message }
else
format.html { render :new }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
60行すこし
class MessagesController < ApplicationController
before_action :set_message, only: [:show, :edit, :update, :destroy]
def index
@messages = Message.all
end
def show; end
def new
@message = Message.new
end
def edit; end
def create
@message = Message.new(message_params)
respond_to do |format|
if @message.save
format.html { redirect_to @message, notice: 'Message was successfully created.' }
format.json { render :show, status: :created, location: @message }
else
format.html { render :new }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @message.update(message_params)
format.html { redirect_to @message, notice: 'Message was successfully updated.' }
format.json { render :show, status: :ok, location: @message }
else
format.html { render :edit }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
def destroy
@message.destroy
respond_to do |format|
format.html { redirect_to messages_url, notice: 'Message was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_message
@message = Message.find(params[:id])
end
def message_params
params.require(:message).permit(:title, :body, :published)
end
end
resource routing にこだわろうという件
メッセージにお気に入りをつけたい
— って要件を考えてみる.
— 特定のメッセージに対してお気に入りにしたりそれ
を外したりできる
— 今回は「誰が」お気に入りをつけるのか? という情報
はサボる
— 本質でないので, 簡単に favorited:boolean の
フラグを Message モデルにつけるだけにしてる.
favorited フラグを用意する
$ ./bin/rails g migration AddFavoritedToMessages favorited:boolean
$ ./bin/rails db:migrate
どのようなアクションを準備すべきか?
resources :messages do
post :add_favorite, on: :member
post :remove_favorite, on: :member
end
$ ./bin/rails routes
Prefix Verb URI Pattern Controller#Action
add_favorite_message POST /messages/:id/add_favorite(.:format) messages#add_favorite
remove_favorite_message POST /messages/:id/remove_favorite(.:format) messages#remove_favorite
...
class MessagesController < ApplicationController
def add_favorite
end
def remove_favorite
end
...
➡ (´・ω・`)…
どのようなアクションを準備すべきか?
resources :messages do
put :favorite, on: :member
delete :favorite, on: :member
end
$ ./bin/rails routes
Prefix Verb URI Pattern Controller#Action
favorite_message PUT /messages/:id/favorite(.:format) messages#favorite
DELETE /messages/:id/favorite(.:format) messages#favorite
...
class MessagesController < ApplicationController
def favorite # favorite & unfavorite ?
end
...
➡ (´・ω・`)……
どのようなアクションを準備すべきか?
resources :messages do
put :favorite, on: :member, action: 'add_favorite'
delete :favorite, on: :member, action: 'remove_favorite'
end
$ ./bin/rails routes
Prefix Verb URI Pattern Controller#Action
favorite_message PUT /messages/:id/favorite(.:format) messages#add_favorite
DELETE /messages/:id/favorite(.:format) messages#remove_favorite
...
class MessagesController < ApplicationController
def add_favorite
end
def remote_favorite
end
...
➡ (´・ω・`)………
ルーティングの設計ガイドライン
— resource(s) で提供されるアクション以外を避ける
— index, show, new, create, edit, update, destroy のみ
— config/routes.rb で get とか post とかをな
るべく使わない.
ルーティングの設計ガイドライン
— 実現することから名詞を探し, 積極的にサブリソース
としてデザインする
— /messages/:message_id/favorite
ルーティングの設計ガイドライン
— 今回の例では /messages/:message_id/
favorite に対して PUT/DELETE で実現
— DELETE だからと言って, 実際に DB 上でデータを
削除する必要はない.
— URI のリソースに対し, DB での永続化方法は全
く別の話.
— DB構成変えるたびに URI構成変えないでしょ?
再考 - ルーティング
resources :messages do
resource :favorite, only: %i[update destroy], module: 'messages'
end
$ ./bin/rails routes
Prefix Verb URI Pattern Controller#Action
message_favorite PATCH /messages/:message_id/favorite(.:format) messages/favorites#update
PUT /messages/:message_id/favorite(.:format) messages/favorites#update
DELETE /messages/:message_id/favorite(.:format) messages/favorites#destroy
...
再考 - コントローラ
# app/controllers/messages/favorites_controller.rb
class < Messages::FavoritesController
before_action :set_message
def update
respond_to do |format|
if @message.update(favorited: true)
format.html { redirect_to @message, notice: 'Favorited.' }
else
format.html { redirect_to @message, alert: 'Favorite failed.' }
end
end
end
def destroy
respond_to do |format|
if @message.update(favorited: false)
format.html { redirect_to @message, notice: 'Unfavorited.' }
else
format.html { redirect_to @message, alert: 'Unfavorite failed.' }
end
end
end
private
def set_message
@message = Message.find(params[:message_id])
end
end
# (json 省略.)
template inheritance 便利だよという件
template inheritance #とは
— ビューはどのディレクトリ/ファイルを探索する?
— app/views/コントローラ名/アクション名.拡張子
みたいなところ?
— ➡ だいたいあってるけどもうひといき
— ➡
❓
template inheritance #とは
— ビューはどのディレクトリ/ファイルを探索する?
— app/views/コントローラ名/アクション名.拡張子
みたいなところ?
— ➡ だいたいあってるけどもうひといき
— ➡ コントローラの継承ツリーがヒントになっている
view template の探索順序
— コントローラの継承ツリー
— MessagesController
— < ApplicationController
— ➡ 以下の順に探索される
1. app/views/messages/*
2. app/views/application/*
view template の探索順序
— 例えば Messages のサブクラスの Foos を作ったら
— FoosController < MessagesController
— ➡ 以下の順に探索される
1. app/views/foos/*
2. app/views/messages/*
3. app/views/application/*
ちょっとためそう
$ mkdir app/views/application
$ mv app/views/messages/index.html.erb app/views/application/
➡ 場所移しても動く.
$ cp app/views/application/index.html.erb app/views/messages/index.html.erb
$ echo '<h2>ほげ</h2>' >> app/views/messages/index.html.erb
➡ messages/ が優先.
みんな ApplicationController を継承してるということは
— app/views/application/ は共通ビューテンプレ
ート置き場に使える.
— しかも, 場合によっては各々のコントローラで上書き
できる.
— これ知らずに app/views/shared/* とか作るより
Rails way かと.
たとえばこんなルーティング
— /messages - メッセージ一覧
— これに対して、 /messages/draft という URL を準
備する
— 基本的には /messages と同じ.
— ただし, published が false のデータでフィルタ
された状態とする.
/messages/draft をつくる - 1/4
app/controllers/messages_controller.rb
def index
- @messages = Message.all
+ @messages = message_scope
end
private
+ def message_scope
+ Message.all
+ end
+
def set_message
/messages/draft をつくる - 2/4
app/controllers/messages/
drafts_controller.rb
class Messages::DraftsController < MessagesController
private
def message_scope
Message.where(published: false)
end
end
/messages/draft をつくる - 3/4
config/routes.rb
Rails.application.routes.draw do
+ namespace :messages do
+ resources :drafts, only: %i[index]
+ end
resources :messages
end
/messages/draft をつくる - 4/4
config/routes.rb
Rails.application.routes.draw do
namespace :messages do
- resources :drafts, only: %i[index]
+ resources :drafts, only: %i[index], path: 'draft'
end
resources :messages
end
➡ 以上
❗
ビューの修正などはなし
まとめ
— scaffold の吐くコントローラは良いコード.
— routes にはなるべく resource(s) だけを記述.
— URIでデザインするリソースと DB永続化は別の話.
— template inheritance をうまく使おう.
— 共通のテンプレートは
app/views/application/ ディレクトリ

Rails Controller Fundamentals

  • 1.
    Rails Controller Fundamentals —とっとるびー第31回 2018-09-24 — https://tottoruby.connpass.com/event/100646/ — @hamajyotan (SAKAGUCHI Takashi)
  • 2.
    さきにお伝え — ちょっとした Railsコードを動かしたりします. 以下 の状態が手元にあると, 一緒に試したりできます. — Ruby 2.5.1 , Rails 5.2.1 で実施しますが, そこまで 厳密に合わせる必要はないです. — gem sqlite3. (rails new で database を指定しない ので既定で使っているだけであり, 行間読んで別 DB を指定できればそれで OK です)
  • 3.
    自己紹介 — SAKAGUCHI Takashi —Office UMMM LLC — Ruby とか Rails とかがちょっとできるおじさんです
  • 4.
    今日話すこと — scaffold よくみてみようという件 —resource routing にこだわろうという件 — template inheritance 便利だよという件
  • 5.
  • 6.
    簡単なアプリを作りつつ $ ruby -v ruby2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux] $ rails -v Rails 5.2.1 $ rails new example --skip-active-storage 出力省略... $ cd example/ $ ./bin/rails s ➡ http://localhost:3000
  • 7.
    簡単なアプリを作りつつ $ ./bin/rails scaffoldmessage title body:text published:boolean $ ./bin/rails db:migrate ➡ http://localhost:3000/ messages
  • 8.
    resource(s) なルーティング config/routes.rb Rails.application.routes.draw do resources:messages end $ rails routes Prefix Verb URI Pattern Controller#Action messages GET /messages(.:format) messages#index POST /messages(.:format) messages#create new_message GET /messages/new(.:format) messages#new edit_message GET /messages/:id/edit(.:format) messages#edit message GET /messages/:id(.:format) messages#show PATCH /messages/:id(.:format) messages#update PUT /messages/:id(.:format) messages#update DELETE /messages/:id(.:format) messages#destroy
  • 9.
    アクションとその意味 Action 意味 index 一覧表示(画面) create新規作成 new 新規作成(画面) edit 編集(画面) show 詳細表示(画面) update 更新 destroy 削除
  • 10.
    html 都合の new/editはこのさい置いとく Resource HTTP Method Action 意味 /messages GET index 一覧表示(画面) /messages POST create 新規作成 /message/:id GET show 詳細表示(画面) /message/:id PATCH/PUT update 更新 /message/:id DELETE destroy 削除
  • 11.
    HTTP のメソッド HTTP Method/messages /message/:id GET index show PUT/PATCH - update DELETE - destroy POST create -
  • 12.
    HTTP のメソッド HTTP Method意図など 安全 冪等 GET リソースの取得 ! ! PUT/PATCH リソースの配置 !❓ DELETE リソースの削除 !❓ POST リソースの生成
  • 13.
    GET — リソースの取得. 常に安全で冪等. —scaffold の実装だと, コレクションへの GET は常に 200 でメンバーへの GET はない場合 404 を返す. def index @messages = Message.all end def show end
  • 14.
    PUT/PATCH — リソースの配置. — scaffoldの実装の場合存在しない場合は 404 を返す が, 定義からすれば ID を明示した merge (insert or update) とした方が適当かも. def update respond_to do |format| if @message.update(message_params) format.html { redirect_to @message, notice: 'Message was successfully updated.' } format.json { render :show, status: :ok, location: @message } else format.html { render :edit } format.json { render json: @message.errors, status: :unprocessable_entity } end end end
  • 15.
    DELETE — リソースの削除. — scaffoldの実装の場合存在しない場合は 404 を返す が, 定義からすれば存在しない場合は何もしないのが 適当かも. def destroy @message.destroy respond_to do |format| format.html { redirect_to messages_url, notice: 'Message was successfully destroyed.' } format.json { head :no_content } end end
  • 16.
    POST — リソースの生成 — 「生成」としているのはコレクションリソースに対 する操作であるため. defcreate @message = Message.new(message_params) respond_to do |format| if @message.save format.html { redirect_to @message, notice: 'Message was successfully created.' } format.json { render :show, status: :created, location: @message } else format.html { render :new } format.json { render json: @message.errors, status: :unprocessable_entity } end end end
  • 17.
    60行すこし class MessagesController <ApplicationController before_action :set_message, only: [:show, :edit, :update, :destroy] def index @messages = Message.all end def show; end def new @message = Message.new end def edit; end def create @message = Message.new(message_params) respond_to do |format| if @message.save format.html { redirect_to @message, notice: 'Message was successfully created.' } format.json { render :show, status: :created, location: @message } else format.html { render :new } format.json { render json: @message.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @message.update(message_params) format.html { redirect_to @message, notice: 'Message was successfully updated.' } format.json { render :show, status: :ok, location: @message } else format.html { render :edit } format.json { render json: @message.errors, status: :unprocessable_entity } end end end def destroy @message.destroy respond_to do |format| format.html { redirect_to messages_url, notice: 'Message was successfully destroyed.' } format.json { head :no_content } end end private def set_message @message = Message.find(params[:id]) end def message_params params.require(:message).permit(:title, :body, :published) end end
  • 18.
  • 19.
    メッセージにお気に入りをつけたい — って要件を考えてみる. — 特定のメッセージに対してお気に入りにしたりそれ を外したりできる —今回は「誰が」お気に入りをつけるのか? という情報 はサボる — 本質でないので, 簡単に favorited:boolean の フラグを Message モデルにつけるだけにしてる.
  • 20.
    favorited フラグを用意する $ ./bin/railsg migration AddFavoritedToMessages favorited:boolean $ ./bin/rails db:migrate
  • 21.
    どのようなアクションを準備すべきか? resources :messages do post:add_favorite, on: :member post :remove_favorite, on: :member end $ ./bin/rails routes Prefix Verb URI Pattern Controller#Action add_favorite_message POST /messages/:id/add_favorite(.:format) messages#add_favorite remove_favorite_message POST /messages/:id/remove_favorite(.:format) messages#remove_favorite ... class MessagesController < ApplicationController def add_favorite end def remove_favorite end ... ➡ (´・ω・`)…
  • 22.
    どのようなアクションを準備すべきか? resources :messages do put:favorite, on: :member delete :favorite, on: :member end $ ./bin/rails routes Prefix Verb URI Pattern Controller#Action favorite_message PUT /messages/:id/favorite(.:format) messages#favorite DELETE /messages/:id/favorite(.:format) messages#favorite ... class MessagesController < ApplicationController def favorite # favorite & unfavorite ? end ... ➡ (´・ω・`)……
  • 23.
    どのようなアクションを準備すべきか? resources :messages do put:favorite, on: :member, action: 'add_favorite' delete :favorite, on: :member, action: 'remove_favorite' end $ ./bin/rails routes Prefix Verb URI Pattern Controller#Action favorite_message PUT /messages/:id/favorite(.:format) messages#add_favorite DELETE /messages/:id/favorite(.:format) messages#remove_favorite ... class MessagesController < ApplicationController def add_favorite end def remote_favorite end ... ➡ (´・ω・`)………
  • 24.
    ルーティングの設計ガイドライン — resource(s) で提供されるアクション以外を避ける —index, show, new, create, edit, update, destroy のみ — config/routes.rb で get とか post とかをな るべく使わない.
  • 25.
  • 26.
    ルーティングの設計ガイドライン — 今回の例では /messages/:message_id/ favoriteに対して PUT/DELETE で実現 — DELETE だからと言って, 実際に DB 上でデータを 削除する必要はない. — URI のリソースに対し, DB での永続化方法は全 く別の話. — DB構成変えるたびに URI構成変えないでしょ?
  • 27.
    再考 - ルーティング resources:messages do resource :favorite, only: %i[update destroy], module: 'messages' end $ ./bin/rails routes Prefix Verb URI Pattern Controller#Action message_favorite PATCH /messages/:message_id/favorite(.:format) messages/favorites#update PUT /messages/:message_id/favorite(.:format) messages/favorites#update DELETE /messages/:message_id/favorite(.:format) messages/favorites#destroy ...
  • 28.
    再考 - コントローラ #app/controllers/messages/favorites_controller.rb class < Messages::FavoritesController before_action :set_message def update respond_to do |format| if @message.update(favorited: true) format.html { redirect_to @message, notice: 'Favorited.' } else format.html { redirect_to @message, alert: 'Favorite failed.' } end end end def destroy respond_to do |format| if @message.update(favorited: false) format.html { redirect_to @message, notice: 'Unfavorited.' } else format.html { redirect_to @message, alert: 'Unfavorite failed.' } end end end private def set_message @message = Message.find(params[:message_id]) end end # (json 省略.)
  • 29.
  • 30.
    template inheritance #とは —ビューはどのディレクトリ/ファイルを探索する? — app/views/コントローラ名/アクション名.拡張子 みたいなところ? — ➡ だいたいあってるけどもうひといき — ➡ ❓
  • 31.
    template inheritance #とは —ビューはどのディレクトリ/ファイルを探索する? — app/views/コントローラ名/アクション名.拡張子 みたいなところ? — ➡ だいたいあってるけどもうひといき — ➡ コントローラの継承ツリーがヒントになっている
  • 32.
    view template の探索順序 —コントローラの継承ツリー — MessagesController — < ApplicationController — ➡ 以下の順に探索される 1. app/views/messages/* 2. app/views/application/*
  • 33.
    view template の探索順序 —例えば Messages のサブクラスの Foos を作ったら — FoosController < MessagesController — ➡ 以下の順に探索される 1. app/views/foos/* 2. app/views/messages/* 3. app/views/application/*
  • 34.
    ちょっとためそう $ mkdir app/views/application $mv app/views/messages/index.html.erb app/views/application/ ➡ 場所移しても動く. $ cp app/views/application/index.html.erb app/views/messages/index.html.erb $ echo '<h2>ほげ</h2>' >> app/views/messages/index.html.erb ➡ messages/ が優先.
  • 35.
    みんな ApplicationController を継承してるということは —app/views/application/ は共通ビューテンプレ ート置き場に使える. — しかも, 場合によっては各々のコントローラで上書き できる. — これ知らずに app/views/shared/* とか作るより Rails way かと.
  • 36.
    たとえばこんなルーティング — /messages -メッセージ一覧 — これに対して、 /messages/draft という URL を準 備する — 基本的には /messages と同じ. — ただし, published が false のデータでフィルタ された状態とする.
  • 37.
    /messages/draft をつくる -1/4 app/controllers/messages_controller.rb def index - @messages = Message.all + @messages = message_scope end private + def message_scope + Message.all + end + def set_message
  • 38.
    /messages/draft をつくる -2/4 app/controllers/messages/ drafts_controller.rb class Messages::DraftsController < MessagesController private def message_scope Message.where(published: false) end end
  • 39.
    /messages/draft をつくる -3/4 config/routes.rb Rails.application.routes.draw do + namespace :messages do + resources :drafts, only: %i[index] + end resources :messages end
  • 40.
    /messages/draft をつくる -4/4 config/routes.rb Rails.application.routes.draw do namespace :messages do - resources :drafts, only: %i[index] + resources :drafts, only: %i[index], path: 'draft' end resources :messages end ➡ 以上 ❗ ビューの修正などはなし
  • 41.
    まとめ — scaffold の吐くコントローラは良いコード. —routes にはなるべく resource(s) だけを記述. — URIでデザインするリソースと DB永続化は別の話. — template inheritance をうまく使おう. — 共通のテンプレートは app/views/application/ ディレクトリ