Hit the books!!

プログラミング学習記録

Scaffoldで作成した図書アプリの画面解説

前回、Scaffoldを使って本を管理する簡単なアプリを作って立ち上げた。

ud-ike.hatenablog.com

今回は画面の基本的な仕組みを勉強します!

一覧画面の仕組み

http://localhost:3000/booksというURIに基づいて図書一覧が表示されるようになっている。

実際にデータを登録すると、このような表示になる。

f:id:ud_ike:20201011144831p:plain

前回書いたとおり、ルーターで指定されているresources :booksは、実際には7つのアクションに相当するルートとして実装されている。

そのうちの「GET /books」に対応するルートが「books#index」であるため、このリクエストに基づいてBooksコントローラーのindexアクションが呼ばれる。

# app/controllers/books_controller.rb のindexアクション部分

  def index
    @books = Book.all
  end

Books.allは、Bookモデルの持つallメソッドを呼び出すことでBookモデルに相当するデータベーステーブル(books)の全データを取得し、ひとつずつインスタンス化して配列化したものを変数@booksに紐付けている。

indexアクションの最後でビュー出力の指示がない場合、該当するコントローラーのアクション名と同じレビューを探し、それを利用してクライアントレスポンスを返す。つまりviews/booksディレクトリにあるindex.html.erbを呼び出している。

# app/views/books/index.html.erb

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

<h1>Books</h1>

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

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

<br>

<%= link_to 'New Book', new_book_path %>

@booksから1件ずつ図書データのインスタンスをbookとして取り出し、その都度titleとdescriptionを取得している。

新規登録の仕組み

新規登録画面はhttp://localhost:3000/books/newで表示される。

f:id:ud_ike:20201011154056p:plain

宛先は「GET /books/new」に対応するアクション「books#new」に相当する。

# app/controllers/books_controller.rb のnewアクション部分

  def new
    @book = Book.new
  end

Book.newでBookモデルの新規のインスタンスを作成し、そのインスタンスオブジェクトを@bookインスタンス変数に対応付けている。

indexアクションと同様、特にビューへの出力指示がないためviews/booksディレクトリにあるnew.html.erbが呼び出される。

# app/views/books/new.html.erb 

<h1>New Book</h1>

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

<%= link_to 'Back', books_path %>

renderはビューを出力するためのメソッドである。ビューの部品としてビューの中に構成されるビューテンプレートを部分テンプレートという。

部分テンプレート

部分テンプレートは_ではじまる名前をつけることで他のテンプレートと区別される。記述する時点ではrender 'form'のように_を外してformだけで指定するが、実際には_form.html.erbという名前の部分テンプレートに対応している。

# app/views/books/_form.html.erb

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

      <ul>
        <% book.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 :description %>
    <%= form.text_area :description %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

部分テンプレートを利用すると、新規登録で入力を行う画面と編集で入力する画面とを同じ形式として共通化することができる。

編集画面を表示するedit.html.erbでも_form.html.erbを呼び出している。

# app/views/books/edit.html.erb

<h1>Editing Book</h1>

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

<%= link_to 'Show', @book %> |
<%= link_to 'Back', books_path %>

再びnew.html.erbのrender 'form', book: @bookに話を戻すと...

「book: @book」というオプションで部分テンプレートに渡す引数を指示している。具体的には、部分テンプレートが持つローカル変数bookにインスタンス変数@bookを対応付けている。

_form.html.erbの中のform_with(model: book, local: true) do |form|にあるbookというローカル変数が、renderで指定される引数のbookに対応する。したがって、遡るとbookにはBooks.newでインスタンス化されたBookモデルのオブジェクトが渡されることになる。

f:id:ud_ike:20201011162154p:plain

createアクション

図書の新規登録を行うにはブラウザに表示された画面からタイトルと説明を入力し、「Create Book」ボタンを押すことで次のアクションのリクエストをRailsサーバに対して行うことになる。

ボタンを押すことによってhttp://localhost:3000/booksに対してHTTPリクエストが行われる。URIは図書一覧と同じだが、HTTPメソッドがPOSTである点が異なる。そのため、ルーターは7つのアクションのうちbooks#createを呼び出すことになる。

# app/controllers/books_controller.rb のcreateアクション部分

 def create
    @book = Book.new(book_params)

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

@book = Book.new(book_params)はBookモデルをインスタンス化している。newアクションのときとは異なり、引数book_paramsを使用して入力された情報を取り込みインスタンス化する。

book_paramsはBooksコントローラーの中でprivateメソッドとして次のように実装されている。

# app/controllers/books_controller.rb のbook_paramsメソッド部分

  private
    ...(省略)
    def book_params
      params.require(:book).permit(:title, :description)
    end

最初のparamsは受信パラメータのオブジェクトである。パラメータとして受け取った入力情報はparamsオブジェクトの中に保存されていると考えるといい。

このパラメータの中にあるBookモデル関連のパラメータのみを抜き出し、それをデータベースに保存できるように許可を与える作業(ストロングパラメータ化)を行っている。ストロングパラメータについては別途まとめようと思う。

books_controller.rbのcreateアクションに話を戻す。

respond_to do |format| ~ endという箇所は、フォーマット(HTMLなどの形式)に基づいてブロック処理を行っている。こちらも詳細は別途まとめる予定。

保存が正しく行われていれば(if @book.save)、redirect_to @bookを実行することになる。redirect_toはリダイレクト処理を行うことを意味し、Railsはredirect_toで指定されている@book(変数が保持する特定のインスタンス)によって、次の宛先(インスタンス内容を表示するルート)を自動で判断して設定する。具体的には、@bookインスタンスの新規で登録された図書IDに基づいて/books/:idのルート形式に相当するルートの宛先を作り出す。

リダイレクト

さらに、リダイレクトの後ろに指示されているオプションnotice: 'Book was successfully created.'は、リダイレクトの宛先に渡すメッセージであり、noticeという変数にセットすることでリダイレクトされたアクションによって出力されるビューテンプレートに渡される。

そうして、リダイレクト先である/books/4(IDが4の場合)というリクエストに基づいて「GET /books/:id」に対応する「books#show」が呼ばれることになる。

ここで疑問が湧く。なんで@bookだけでshowって書いてないのにshowにとぶの...??

調べたら、ルーティング情報のRrefixがbookになっているところをみればわかった!!

f:id:ud_ike:20201011200907p:plain

ルーティング情報について↓

ud-ike.hatenablog.com

話を戻して、showアクションについて。

# app/controllers/books_controller.rb のshowアクション部分

  def show
  end

before_action

上のshowアクションは何もしていないように見えるが、Booksコントローラーの最初の行に次のような記述がある。

# app/controllers/books_controller.rb の先頭部分

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

before_actionはアクションの実行前に実行するメソッドを指示する。onlyで指定されるオプション[:show, :edit, :update, :destroy]によってshowアクションが有効になっている。つまり、showアクションは先にset_bookメソッドを実行してから実行されることになる。

set_bookメソッドはprivateメソッドとして定義されている。

# app/controllers/books_controller.rb のset_bookメソッド部分

  private
    def set_book
      @book = Book.find(params[:id])
    end

Book.find(params[:id])という記述は、

  • Bookモデルを利用して
  • リダイレクトの結果受信したパラメータのハッシュであるparamsオブジェクトを使用し
  • params[:id]で参照するidの値(この場合は4)に相当する図書をデータベースの該当テーブルから取得する

ことを意味している。つまり@book = Book.find(params[:id])は、受け取ったidの値に相当する図書のインスタンスを@bookに関係付けていることを意味する。

このbefore_actionのあとshowアクションが実行されるが、記述がないため同じ名前のshow.html.erbを呼び出す。

# app/views/books/show.html.erb
 
<p id="notice"><%= notice %></p>

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

<p>
  <strong>Description:</strong>
  <%= @book.description %>
</p>

<%= link_to 'Edit', edit_book_path(@book) %> |
<%= link_to 'Back', books_path %>

最初の行の<%= notice %>で、変数noticeに渡された成功メッセージがブラウザに表示される。

f:id:ud_ike:20201011180740p:plain