Ruby on Rails ~レールの路線図~ Part 2 [Ruby3, Rails6]

2006.1.21 に Rubyist九州 Meeting 第1回にて発表した, Ruby on Rails のスライドです。2005年12月に Rails 1.0 が出ました。その前後, 同月に初めて Railsに触ったので、まだほんの初歩的、序の口のところです。

[2021.5] ざっと現代の状況に更新。Ruby 3.0 + Ruby on Rails v6.1. サンプルコード netsphere / rails-examples · GitLab

  1. ページ1 アプリケィションひな形の起動まで。
  2. このページ. 最初のDBアプリケィションづくり。
  3. ページ3 多対多リレーションシップ, トランザクション、ほか。

rails generate コマンド

ほしたら、順番に作っていこう。まずはひな形を生成。

rails generate サブコマンド コマンドでいろいろ自動生成. サブコマンド一覧;

  • Rails:

    application_record assets benchmark channel controller generator helper integration_test jbuilder job mailbox mailer migration model resource scaffold scaffold_controller system_test task

  • ActiveRecord:

    active_record:application_record

  • RackProfiler:

    rack_profiler:install

  • TestUnit:

    test_unit:channel test_unit:generator test_unit:install test_unit:mailbox test_unit:plugin

Scaffold (足場)

Rails では, リソース (オブジェクト) に注目して, アプリケィションを作っていく。

Scaffold は建築現場などの足場という意味で、建てるビルそのものではない。Rails では自動生成させたコードを出発点にして修正して, ちゃんとしたものを作っていく。

Usage:
  rails generate scaffold NAME [field[:type][:index] field[:type][:index]] [options]

NAME は小文字の単数形. やってみよう.

次のファイルが作られる. app/models ディレクトリには単数形のファイルが, app/controllers には 複数形_controller.rb が, app/views には複数形のディレクトリが掘られて, コマンドに対応する ERBテンプレートが生成される。

$ rails g scaffold article title:string body:text
      invoke  active_record
      create    db/migrate/20210709151353_create_articles.rb
      create    app/models/article.rb
      invoke    test_unit
      create      test/models/article_test.rb
      create      test/fixtures/articles.yml
      invoke  resource_route
       route    resources :articles
      invoke  scaffold_controller
      create    app/controllers/articles_controller.rb
      invoke    erb
      create      app/views/articles
      create      app/views/articles/index.html.erb
      create      app/views/articles/edit.html.erb
      create      app/views/articles/show.html.erb
      create      app/views/articles/new.html.erb
      create      app/views/articles/_form.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/articles_controller_test.rb
      create      test/system/articles_test.rb
      invoke    helper
      create      app/helpers/articles_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/articles/index.json.jbuilder
      create      app/views/articles/show.json.jbuilder
      create      app/views/articles/_article.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/articles.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

config/routes.rb ファイルにルーティングが追加される。

モデル 一周目

Active Record パタン

上述のとおり、Ruby on Rails における「モデル」は、データベースの表と1:1で対応する Ruby クラスを作る。

データをリレーショナルデータベースへ保存する設計は、Active Record と Data Mapper がある。

Active Record パタン
Data Mapperパタン, 出典 https://martinfowler.com/eaaCatalog/dataMapper.html

Rails が採用する Active Record パタンでは、一つ一つのレコード (表では行として表される) が振る舞い、つまり操作するメソッドも持つ。他方、Data Mapper パタンでは、レコードクラスは振る舞いのためのメソッドは持たない。Active Record 実装では、振る舞いのためのメソッドを定義する基底クラスから派生させる。そのため、ほかのクラスから派生させることができないが、いちいち別に操作のためのクラスが出てこないので、記述量は減る。

Data Mapper パタンであっても、ほかのフレームワーク・O/Rマッパでは, 下手な基底クラスを使うと上手く動かないこともままある。ほかのO/R マッパには、開発時にどちらか選べるようなものもある。そういうときは Active Record のほうがメリット大きいと思う。

複数の表を跨ぐようなビジネスロジックは, Rails でのモデル (=表) の上に, 別クラスとして構築する。

モデル (2)

実際のソースコードを見ていこう。

  • それぞれのモデルのクラス名は、単数形、PascalCase
  • モデルクラスのファイル名は、単数形を小文字アンダースコア化したもの (snake_case).

Migration

rails generate コマンドでひな形を生成すると, migration コードも自動生成される。列と NOT NULL 制約などを記述する。

db/migrate/20210709151353_create_articles.rb ファイル:

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # articles テーブルを生成する.
  3. class CreateArticles < ActiveRecord::Migration[6.1]
  4. # change メソッドの代わりに, up / down メソッドも使える.
  5. def change
  6. create_table :articles do |t|
  7. t.string :title, null:false
  8. t.text :body, null:false
  9. t.timestamps
  10. end
  11. end
  12. end

create_table メソッドでテーブルを作成する。null:falseNOT NULL 制約.

rake コマンドで, データベーススキーマのヴァージョンを上げ下げできる。

rake サブコマンド 説明
db:migrate Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE...
db:migrate:down Runs the "down" for a given migration VERSION. 一つ戻したいときは db:rollback.
db:migrate:redo Rolls back the database one migration and re-migrates up (opti...
db:migrate:status Display status of migrations
db:migrate:up Runs the "up" for a given migration VERSION

では、データベーススキーマのヴァージョンを上げてみよう. hello_app ディレクトリにて、コマンドを叩く。

$ rake db:migrate
== 20210709151353 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0046s
== 20210709151353 CreateArticles: migrated (0.0049s) ==========================

$ rake db:migrate:status
database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20210709151353  Create articles

app/models/article.rb ファイル: データベースの表と対応したモデルクラス.

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # 記事
  3. class Article < ApplicationRecord
  4. # 検証: 列は複数並べてもよい.
  5. validates :title, presence:true
  6. # カスタム検証
  7. validate :my_validation
  8. # Callback として, before_save, before_validation などがある.
  9. before_save :normalize_text
  10. private
  11. # for validate.
  12. def my_validation
  13. if title == "NG"
  14. errors[:title] << "値がNG!"
  15. end
  16. end
  17. # for before_save
  18. def normalize_text
  19. self.title = title.unicode_normalize :nfkc # 試しに
  20. self.body = body.unicode_normalize :nfc
  21. end
  22. end

モデルクラスは, モデルに共通の ApplicationRecord クラスから派生させる。ActiveRecord::Base から派生している。

Rails では列を明示的に書かなくてもよい。クラスのプロパティが自動的に生成される。

バリデーション (検証) は、出来合いの内容なら validates クラスメソッドで指定するだけ。メソッド定義してもよい。

app/models/application_record.rb ファイル: 共通のコードを書く.

Ruby
[RAW]
  1. class ApplicationRecord < ActiveRecord::Base
  2. self.abstract_class = true
  3. end

基底クラスである ActiveRecord::Base が振る舞いを定義している。保存(新規, 更新) save(), 削除 destroy() など。

検索はクラスメソッドの find() など。

DBテーブル

リレーショナルデータベースに生成されるテーブル名は, 複数形、小文字アンダースコア (snake_case). SQL は大文字小文字を区別しないので、これしかない。

主キーは自動的に id という名前で生成される。整数型。MySQLでは、auto_increment も必要.

外部キー (FK) は、'単数形+_id'という名前にする。ex) product_id

SQLite だと次のスキーマが生成される。

CREATE TABLE "articles" (
  "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  "title" varchar NOT NULL,
  "body" text NOT NULL,
  "created_at" datetime(6) NOT NULL,
  "updated_at" datetime(6) NOT NULL
  );

コントローラ

Action Pack

Action Packパッケージがリクエストからレスポンスまでのルーティングを担当。

名前付け
コントローラ名:小文字のアンダースコア (snake_case). 'admin/credit_cards' など'/' で区切って長くできる.
クラス名:コントローラ名をPascalCase にして, + Controller。パスの部分は Rubyモジュール名になる. コントローラ名が admin/credit_cards なら, Admin::CreditCardsController
ファイル名:クラス名小文字アンダースコア化 (snake_case).

アクションの決定

Rails では, URLとHTTP動詞から, 処理するコントローラとアクションが決まる。コントローラは 1:1 で対応するクラスになる。アクションはそのクラスのメソッド。

コントローラとアクションから, クライアントに戻すためのデフォルトビューが自動的に決まる。

Rails はリソース中心に組み立てられている。config/routes.rb にルーティングを記述する。このファイルに単に次のように書くと,

Ruby
[RAW]
  1. Rails.application.routes.draw do
  2. resources :articles
  3. end

次のURL とアクションの対応が作られる。rails routes コマンドで一覧表示できる。※以前は rake routes コマンドだったが、Rails v6 で取り除かれた。rake routes の解説は古い。

Prefix HTTP動詞 URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH or PUT /articles/:id(.:format) articles#update
DELETE/articles/:id(.:format) articles#destroy

URI が同じであっても、HTTP動詞によって、呼び出されるアクションが変わってくる。現代のRESTful API デザインでは, update のためには PUT のほうがメジャー。Rails は, 過去との互換性のため, PATCH (RFC 5789; 2010年) も受け付ける。

URI Pattern 内の :id などのパラメータは, Rubyコード側から params[:名前] で得られる。

/ の場合にリダイレクトするよう、次のようにする。

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. Rails.application.routes.draw do
  3. resources :articles
  4. # For details on the DSL available within this file,
  5. # see https://guides.rubyonrails.org/routing.html
  6. # リダイレクトさせる
  7. # 別のコントローラで表示させる場合は、次のようにする。
  8. # root to: "welcome#index"
  9. root to: redirect("/articles/")
  10. end

コントローラクラス

アプリケィションで共通の基底クラス. ActionController::Base から派生させる。

app/controllers/application_controller.rb:

Ruby
[RAW]
  1. class ApplicationController < ActionController::Base
  2. end

基本は, リソースごとにコントローラクラスを作る。まず先に、コードを示す。

app/controllers/articles_controller.rb:

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # 記事コントローラ
  3. class ArticlesController < ApplicationController
  4. # 各アクション (メソッド) の前に呼び出される.
  5. before_action :set_article, only: %i[ show edit update destroy ]
  6. # GET /articles or /articles.json
  7. def index
  8. @articles = Article.all
  9. end
  10. # GET /articles/1 or /articles/1.json
  11. def show
  12. end
  13. # GET /articles/new
  14. def new
  15. @article = Article.new
  16. end
  17. # GET /articles/1/edit
  18. def edit
  19. end
  20. # POST /articles or /articles.json
  21. def create
  22. @article = Article.new(article_params)
  23. begin
  24. @article.save!
  25. rescue ActiveRecord::RecordInvalid
  26. render :new, status: :unprocessable_entity
  27. return
  28. end
  29. redirect_to @article, notice: "Article was successfully created."
  30. end
  31. # PATCH/PUT /articles/1 or /articles/1.json
  32. def update
  33. begin
  34. @article.update!(article_params)
  35. rescue ActiveRecord::RecordInvalid
  36. render :edit, status: :unprocessable_entity
  37. return
  38. end
  39. redirect_to @article, notice: "Article was successfully updated."
  40. end
  41. # DELETE /articles/1 or /articles/1.json
  42. def destroy
  43. @article.destroy
  44. redirect_to articles_url, notice: "Article was successfully destroyed."
  45. end
  46. private
  47. # Use callbacks to share common setup or constraints between actions.
  48. def set_article
  49. @article = Article.find(params[:id])
  50. end
  51. # Only allow a list of trusted parameters through.
  52. def article_params
  53. params.require(:article).permit(:title, :body)
  54. end
  55. end

コントローラからモデル操作

モデルオブジェクトのメソッド

  • self.find(主キー)
  • self.new(). ActiveRecord オブジェクトの生成。まだ保存しない。
  • 保存: save(). 基本的に save!() のほうを使う。後者は, 検証に失敗したとき ActiveRecord::RecordInvalid 例外を発生する。
  • 列の値の更新: update_attributes(hash) は非推奨。update(hash) に置き換わった。さらに, 基本的に update!(hash) を使う. save!() と同様に, 検証に失敗したとき ActiveRecord::RecordInvalid 例外を発生する。

    フィールドを更新して、saveも行う

  • destroy

コントローラのメソッド

  • render :action=>'アクション名'
  • paginate :users, :per_page=>件数
  • redirect_to :action=>'アクション名'
  • flash[:notice]
  • params[:パラメータ名]

●●TODO: 更新.

ビュー

最後に, ビューを作って、レスポンスを返せるようにする。

Webpacker

Rails 6 から, デフォルトで, Webpack v5 を wrap する 'webpacker' gem を利用する。(Rails 5 ではオプション.)

どうも Webpacker は人気がないようで、素の Webpack を使う方がいいようだが、とりあえず、Rails のデフォルトに近い状態で動かす。

ビルドに yarn コマンドが必要。

# npm install --global yarn

Gemfile で Turbolinks を外したので、app/views/layouts/application.html.erb ファイルも, 'data-turbolinks-track' を外す。

設定ファイルを更新。app/javascript/packs/application.js

JavaScript
[RAW]
  1. // This file is automatically compiled by Webpack, along with any other files
  2. // present in this directory. You're encouraged to place your actual application
  3. // logic in a relevant structure within app/javascript and only use these pack
  4. // files to reference that code so it'll be compiled.
  5. import Rails from "@rails/ujs"
  6. //import Turbolinks from "turbolinks"
  7. import * as ActiveStorage from "@rails/activestorage"
  8. import "channels"
  9. Rails.start()
  10. //Turbolinks.start()
  11. ActiveStorage.start()

ビルド.

$ rails webpacker:install
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js
      create  config/webpack/production.js
      create  config/webpack/test.js
Copying postcss.config.js to app root directory
      create  postcss.config.js
Copying babel.config.js to app root directory
      create  babel.config.js
Copying .browserslistrc to app root directory
      create  .browserslistrc
The JavaScript app source directory already exists
       apply  /opt/rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/webpacker-5.4.0/lib/install/binstubs.rb
  Copying binstubs
       exist    bin
      create    bin/webpack
      create    bin/webpack-dev-server
      append  .gitignore
Installing all JavaScript dependencies [5.4.0]
         run  yarn add @rails/webpacker@5.4.0 from "."
yarn add v1.22.10
info No lockfile found.
[1/4] Resolving packages...
以下略.

ERBファイル

ファイル名:app/views/パス/コントローラ名/アクション名.html.erb

サーバから HTML を返す部分は, ERB テンプレートで作る。

app/views/articles ディレクトリ:

  • _form.html.erb
  • edit.html.erb
  • index.html.erb
  • new.html.erb
  • show.html.erb

ビュー (2)

  • ERBで記述
  • タグの生成(ActionView)
    • start_form_tag :action => 'update', :id => @user
      • <form action="/users/update/1" method="post">
      • コントローラのインスタンス変数が見える
    • submit_tag 'ボタンテキスト'
    • end_form_tag
    • link_to 'リンクテキスト', :action => 'show', :id => @user
    • text_field 'オブジェクト', 'メソッド'

app/views/articles/index.html.erb

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

<h1>Articles</h1>

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

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

<p><%= link_to 'New Article...', new_article_path %>

app/views/articles/show.html.erb

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

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

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

<%= link_to 'Edit...', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

部分レンダリングなど

  • render 'partial_form'

    _partial_form.html.erb ファイルの内容を出力

  • error_messages_for 'user'
  • HTMLエスケープ
    • <%=h ... %> XSSを発生させないために、忘れないこと。 廃れた。現代ではわざわざエスケープ指示しなくてよい。
  • モデルオブジェクトのメソッド
    • content_columns

new.html.erbedit.html.erb は、タイトルを除き、まったく同じ。

<h1>New Article</h1>

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

_form.html.erb

<% # Rails5 から form_with. 以前は form_for / form_tag だった.
   # url: オプションを指定 (以前の form_tag), or
   # model: オプションを指定 (以前の form_for).
  %>

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

      <ul>
        <% article.errors.each do |error| %>
          <li><%= error.full_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">
<% # new (create) or edit (update) 判定
  %>
    <%= link_to 'キャンセル', article.persisted? ? @article : articles_path %>
    <%= form.submit %>
  </div>
<% end %>

実行!

動くことを確認!