Just do IT

思うは招く

Ruby on Rails scaffold 基本 part1

Scaffoldでアプリの雛形を作る

  • RailsにはScaffolding機能があらかじめ備わっている
  • データを参照、登録、更新、削除をするための一連の画面が生成される機能のこと
  • モデルクラスやマイグレーションファイルも作ってくれる
rails g scaffold リソース名 カラム名1:データ型1,カラム名2:データ型2

ためしにやってみる。

rails g scaffold memo title:stirng body:text

すると

Running via Spring preloader in process 12251
      invoke  active_record
      create    db/migrate/20200117013020_create_memos.rb
      create    app/models/memo.rb
      invoke    test_unit
      create      test/models/memo_test.rb
      create      test/fixtures/memos.yml
      invoke  resource_route
       route    resources :memos
      invoke  scaffold_controller
      create    app/controllers/memos_controller.rb
      invoke    erb
      create      app/views/memos
      create      app/views/memos/index.html.erb
      create      app/views/memos/edit.html.erb
      create      app/views/memos/show.html.erb
      create      app/views/memos/new.html.erb
      create      app/views/memos/_form.html.erb
      invoke    test_unit
      create      test/controllers/memos_controller_test.rb
      create      test/system/memos_test.rb
      invoke    helper
      create      app/helpers/memos_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/memos/index.json.jbuilder
      create      app/views/memos/show.json.jbuilder
      create      app/views/memos/_memo.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/memos.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

たくさんのファイルが生成される。

rails routes

でルーティングが確認できる。

モデルクラスを作成

k-koh.hatenablog.com

rails db:migrate

マイグレーションをする。

テストのためにフィクスチャをする。test/fixtures/memos.ymlで中身を確認し、好きなように変更する。

# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
  id: 1
  title: '今日は上半身の筋トレをした'
  body: '新しいベンチプレスを試してみた'

two:
  id: 2
  title: '背中トレ'
  body: 'やっぱりデッドリフトはフルレンジがいいわ'

テストデータを読み込む。

rails db:fixtures:load

これでrails s

rails s

仮想環境なら

rails s -b 0.0.0.0

でサーバーを起動。

http://192.168.33.10:3000/memos

へ移動すると簡素なウェブアプリケーションがすでにできている。素晴らしい・・・。

詳しく

app/controllers/memos_controller.rb

class MemosController < ApplicationController
  before_action :set_memo, only: [:show, :edit, :update, :destroy]

  # GET /memos
  # GET /memos.json
  def index
    @memos = Memo.all
  end

  # GET /memos/1
  # GET /memos/1.json
  def show
  end

  # GET /memos/new
  def new
    @memo = Memo.new
  end

  # GET /memos/1/edit
  def edit
  end

  # POST /memos
  # POST /memos.json
  def create
    @memo = Memo.new(memo_params)

    respond_to do |format|
      if @memo.save
        format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
        format.json { render :show, status: :created, location: @memo }
      else
        format.html { render :new }
        format.json { render json: @memo.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /memos/1
  # PATCH/PUT /memos/1.json
  def update
    respond_to do |format|
      if @memo.update(memo_params)
        format.html { redirect_to @memo, notice: 'Memo was successfully updated.' }
        format.json { render :show, status: :ok, location: @memo }
      else
        format.html { render :edit }
        format.json { render json: @memo.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /memos/1
  # DELETE /memos/1.json
  def destroy
    @memo.destroy
    respond_to do |format|
      format.html { redirect_to memos_url, notice: 'Memo was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_memo
      @memo = Memo.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def memo_params
      params.require(:memo).permit(:title, :body)
    end
end

index

  def index
    @memos = Memo.all
  end

app/views/memos/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Memos</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @memos.each do |memo| %>
      <tr>
        <td><%= memo.title %></td>
        <td><%= memo.body %></td>
        <td><%= link_to 'Show', memo %></td>
        <td><%= link_to 'Edit', edit_memo_path(memo) %></td>
        <td><%= link_to 'Destroy', memo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Memo', new_memo_path %>

<p id="notice"><%= notice %></p>: noticeは特殊な変数で、コントローラ側の処理の結果、ユーザーに何かしらの通知メッセージを一度だけ表示したい、といった用途で使われる。

<td><%= link_to 'Show', memo %></td>
  • link_to: Railsが提供するもので、リンクを設置するaタグを出力する
    • , memoでリンク先を設定している
    • memo_path(memo.id)の省略形である点に注意
    • <a href="/memo/1">Show</a>というHTMLとして出力される
  • このようにビューで使えるメソッドのことヘルパーメソッドと呼ぶ
<td><%= link_to 'Edit', edit_memo_path(memo) %></td>
  • edit_memo_path(memo):
    • <a href="/memo/1/edit">Edit</a>というHTMLで出力される
    • 引数(memo)(memo.id)の省略形
<td><%= link_to 'Destroy', memo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  • link_to 'Destroy': Destroyへのリンク
  • memo: memo_path(memo.id)の省略形でリンク先
  • method: :delete: リクエストメソッドにdeleteを指定
    • data-method属性を付与し、その値をdeleteにする
  • data: { confirm: 'Are you sure?' }:
    • aタグにdata属性を付与するもので、{}のブロックにハッシュで記述する
    • aタグにdata-confirm属性を付与し、その値を「Are you sure?」にしている

こんなHTMLになる。

<a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/memos/1">Destroy</a>

showアクションを確認

class MemosController < ApplicationController
  before_action :set_memo, only: [:show, :edit, :update, :destroy]

  # GET /memos
  # GET /memos.json
  def index
    @memos = Memo.all
  end

  # GET /memos/1
  # GET /memos/1.json
  def show
  end
  • showメソッドには処理が書かれていないが、before_actionメソッドに書かれている
  • before_action: アクションを呼ぶ前に特定の処理をしたい場合に用意されている
before_action メソッド名, 条件ハッシュ

before_action :set_memo, only: [:show, :edit, :update, :destroy]
  • メソッド名: コントローラー内のメソッドをシンボルで記述する
  • 条件ハッシュ: 適用するアクションの条件をハッシュで記述する
    • キーにonly/exceptのいずれかを指定し、値にはアクション名のシンボルを指定
    • onlyでは、指定したアクションにのみ適用される
    • expectでは、指定したアクション以外のすべてのアクションに適用される
  • show,edit,update,destroyメソッドが呼ばれる前に、set_memoメソッドが呼ばれている
    def set_memo
      @memo = Memo.find(params[:id])
    end
  • URLのidを取得し、Memo.findでデータをとってきて、@memoに代入している
  • Memo.find(params[:id])では、memosテーブルのidがparams[:id]に一致するレコードの、Memoモデルクラスのインスタンスを取得する
  • @memoにレコードを代入することにより、レコードごとの表示画面を動的に生成することができる

対応するファイル: app/views/memos/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @memo.title %>
</p>

<p>
  <strong>Body:</strong>
  <%= @memo.body %>
</p>

<%= link_to 'Edit', edit_memo_path(@memo) %> |
<%= link_to 'Back', memos_path %>

それぞれHTMLにすると。

<%= link_to 'Edit', edit_memo_path(@memo) %>
=> <a href="/memos/1/edit">Edit</a>
<%= link_to 'Back', memos_path %>
=> <a href="/memos">Back</a>

こんな感じになる。

メモの登録

app/controllers/memos_controller.rb

  # GET /memos/new
  def new
    @memo = Memo.new
  end

新しいメモ@memoを登録し、ビューで利用できるようにしている。

app/views/memos/new.html.erb

<h1>New Memo</h1>

<%= render 'form', memo: @memo %>

<%= link_to 'Back', memos_path %>
  • render: ここではformという部分テンプレートを呼び出している
    • 部分テンプレートとは、ビューから別に切り出されたビューファイルのこと
    • app/views/memos/_form.html.erbが呼び出されている
  • 登録や更新をするとき、入力フォームに違いがない場合は、このようにrenderメソッドで部分テンプレートを呼び出すことでビューを部品化して共通に使える
<%= render '部分テンプレート名', キー1: 値1, キー2: 値2,,, %>

=> <%= render 'form', memo: @memo %>
  • _form.html.erbの中で、アクションメソッドで用意された@memoに、memo変数でアクセスできる
  • memo変数は、_form.html.erbでモデルに関連づく入力フォームを生成するために使う

app/views/memos/_form.html.erb

<%= form_with(model: memo, local: true) do |form| %>
  <% if memo.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(memo.errors.count, "error") %> prohibited this memo from being saved:</h2>

      <ul>
        <% memo.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
  • form_with: 引数の形式にしたがって、さまざまなフォームタグを出力するヘルパーメソッドで、do~endで囲むブロック形式で使う
<%= form_with(model: モデルクラスのインスタンス, local: 真偽値) do |form| %>
<% end %>

<%= form_with(model: memo, local: true) do |form| %>
<% end %>
  • modelの引数には、フォームに紐付けるモデルクラスのインスタンスを指定
    • 配下のフォーム要素に、モデルクラスの値を反映できる
  • local: trueは、Ajaxによるデータ更新を許可するかどうかの設定

app/views/memos/_form.html.erbがHTMLとして出力された場合。

<div>
      <h1>New Memo</h1>

<form action="/memos" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="aPI4hvB5SFzTI68oIgE091pVt0bzfchWcirN+FoHJd+K2oj5urRrJBy9fktMURqFcd0iMOmmOg1dhlqdfT0ZEw==">

  <div class="field">
    <label for="memo_title">Title</label>
    <input type="text" name="memo[title]" id="memo_title">
  </div>

  <div class="field">
    <label for="memo_body">Body</label>
    <textarea name="memo[body]" id="memo_body"></textarea>
  </div>

  <div class="actions">
    <input type="submit" name="commit" value="Create Memo" data-disable-with="Create Memo">
  </div>
</form>

<a href="/memos">Back</a>

    </div>

見ていく。

<input type="hidden" name="authenticity_token" value="aPI4hvB5SFzTI68oIgE091pVt0bzfchWcirN+FoHJd+K2oj5urRrJBy9fktMURqFcd0iMOmmOg1dhlqdfT0ZEw==">

Railsが自動で生成するCSRFトークン。

label ビューヘルパー

app/views/memos/_form.html.erb

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

# 出力
<div class="field">
    <label for="memo_title">Title</label>
    <input type="text" name="memo[title]" id="memo_title">
  </div>
  • for属性モデル名_labelの引数という形式で生成される
    • <label for="memo_title">Title</label>
  • 文字列は制定されたフィールド名の先頭を大文字にしたもの

text_field ビューヘルパー

app/views/memos/_form.html.erb

  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

# 出力
<div class="field">
    <label for="memo_title">Title</label>
    <input type="text" name="memo[title]" id="memo_title">
  </div>
<%= form.text_field :引数, 属性名1: 値1, 属性名2: 値2, ... %>
  • テキストボックス用のinputタグは、type属性がtextとなる
  • name属性: オブジェクト名[引数]
  • id属性: モデル名_カラム名
  • 追加属性を指定したい場合は属性名: 値

text_are ビューヘルパー

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>
<div class="field">
    <label for="memo_body">Body</label>
    <textarea name="memo[body]" id="memo_body"></textarea>
  </div>
  • name属性: オブジェクト名[引数]

submit ビューヘルパー

  <div class="actions">
    <%= form.submit %>
  </div>
<div class="actions">
    <input type="submit" name="commit" value="Create Memo" data-disable-with="Create Memo">
  </div>
  • input typeのsubmit
  • name属性: commitがデフォルト
  • value: "Create モデル名"
  • data-disable-with: "Create モデル名"

createメソッドを確認

app/controllers/memos_controller.rb

  def create
    @memo = Memo.new(memo_params)

    respond_to do |format|
      if @memo.save
        format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
        format.json { render :show, status: :created, location: @memo }
      else
        format.html { render :new }
        format.json { render json: @memo.errors, status: :unprocessable_entity }
      end
    end
  end
  • @memo = Memo.new(memo_params)では、memo_paramsメソッドの戻り値を引数として、Memoモデルクラスのインスタンスを作成している
  • memo_paramsメソッドは同一ファイルのprivateメソッド内で定義されている
    • フォームから送信されたデータをparamsメソッドで取得している
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_memo
      @memo = Memo.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def memo_params
      params.require(:memo).permit(:title, :body)
    end
  • params.require/permit: フォームから送信されたデータを指定したものだけに限定するための仕組み
  • Strong Parametersと呼ばれる
    • 例えば、不正な入力フォームから値が送信されても、更新データを日記タイトルと日記本文に限定するため、別IDの日記データが更新されてしまうのを防げる
  • requireの引数にはモデル名を指定
  • permitの引数にはカラム名を指定

params.require(:memo).permit(:title, :body)でこんなハッシュデータが得られる。

{
    'title': 'あああああ',
    'body': 'ららららら'
}

これをDBに保存している。

### respond_toメソッド

`app/controllers/memos_controller.rb`
respond_to do |format|
  if @memo.save
    format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
    format.json { render :show, status: :created, location: @memo }
  else
    format.html { render :new }
    format.json { render json: @memo.errors, status: :unprocessable_entity }
  end
end
- アクセスURLの末尾の拡張子によって処理を分ける場合に使用する
- ブロック引数に拡張子をとり、ブロック内で拡張子ごとの処理をブロックで定義する
- Railsでは拡張子に何も指定されてない場合、フォーマットはhtmlと判断される
- 末尾に「.json」がついている場合はフォーマットはJSONと判断される
  if @memo.save
    format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
    format.json { render :show, status: :created, location: @memo }
  else
- format.html { リクエストフォーマットがhtmlの場合に実行する処理}
    - まずsaveメソッドで保存
    - saveメソッドはデータの保存に成功したかどうかをtrue/falseで返すので、その値に応じて実行する処理を分岐している
    - 正常にデータが保存された場合は(trueが返った場合)、`format.html { redirect_to @memo, notice: 'Memo was successfully created.' }`が実行される

`format.html { redirect_to @memo, notice: 'Memo was successfully created.' }`

- @memoにリダイレクトしている
    - RailsアプリのURLの省略形
    - いま保存したデータの表示画面を意味する
- noticeに移動先で出力するメッセージをハッシュ形式で指定する
    - このメッセージのことをフラッシュメッセージと呼ぶ
    - ビュー側でもnotice変数で参照できる

#### JSON形式で呼ばれた場合

format.json { render :show, status: :created, location: @memo }

- JSON形式でアクセスされると呼び出される処理
- renderメインには:showが指定されている
    - `app/views/memos/show.json.jbuilder`が呼ばれる
- `status: :created`: Webサーバからの応答結果を表す
    - この場合、内部的には201のHTTPステータスコードが返る
    - 201はリクエストが成功し、新しいデータが作成されたことを表す
- `location: @memo`: 新しく登録されたメモデータのURLを表す


`app/views/memos/show.json.jbuilder`

json.partial! "memos/memo", memo: @memo

json.partial! "部分テンプレート名", キー1: 値, ...

- `memos/memo`: `app/views`ディレクトリからのパスを表す
    - `app/views/memos/_memo.json.jbuilder`が呼ばれる
- `memo: @memo`: 部分テンプレートに対して、memo変数で@memoを渡す


`app/views/memos/_memo.json.jbuilder`

json.extract! memo, :id, :title, :body, :created_at, :updated_at json.url memo_url(memo, format: :json)

- `json.extract!`: モデルオブジェクトのカラム要素を指定して値を列挙する

`json.url memo_url(memo, format: :json)`

- urlというキーにたいして、`memo_url(memo, format: :json)`という値を指定する

`~:3000/memos/1.json`にアクセスすると、jsonファイルが返る。

{"id":1,"title":"今日は上半身の筋トレをした","body":"新しいベンチプレスを試してみた","created_at":"2020-01-17T11:04:23.419+09:00","updated_at":"2020-01-17T11:04:23.419+09:00","url":"http://192.168.33.10:3000/memos/1.json"}

### else以降

`app/controllers/memos_controller.rb`
  else
    format.html { render :new }
    format.json { render json: @memo.errors, status: :unprocessable_entity }
  end
- JSON形式で日記データの保存に失敗した場合に呼ばれる
- `render json: @memo.errors`: すべてのエラーメッセージをJSON形式で返す
- `status: :unprocessable_entity`: 422ステータスコードを表し、処理できなかったことを意味する