🍅

Rails Scaffoldを使用して簡単にCRUD機能を作成する

2024/06/21に公開

はじめに

scaffoldを使用して、RailsのプロジェクトにCRUD機能を追加してみます。
ビューの一部は生成されたものから、少し修正しています。

Scaffold

scaffoldはRailsに備わっているコマンドの1つです。使用すると、指定されたモデルに基づいてCRUD操作を行うための一連のファイル(モデル、ビュー、コントローラー、マイグレーション、テストなど)が自動的に生成されます。

bundle exec rails generate scaffold <モデル名> <カラム名:型> <カラム名:型>

今回はこちらを実行します。

bundle exec rails generate scaffold User name:string email:string

生成されたファイルは以下の通りです。
今回ビューは、html2slimをインストールしているのでslim形式でファイルが作成されています。

app/
├── controllers/
│   └── users_controller.rb  # CRUDアクションが定義
├── models/
│   └── user.rb  
├── helpers/
│   └── users_helper.rb  
├── views/
│   └── users/
│       ├── index.html.slim  # 全ユーザーのリストを表示
│       ├── index.json.jbuilder  # 全ユーザーのリストをJSON形式で表示
│       ├── edit.html.slim  # ユーザーの編集フォーム
│       ├── show.html.slim  # ユーザーの詳細情報を表示
│       ├── show.json.jbuilder  # ユーザーの詳細情報をJSON形式で表示
│       ├── _user.json.jbuilder  # 個別ユーザーのデータをJSON形式で部分テンプレート
│       ├── new.html.slim  # 新しいユーザーを作成するフォーム
│       └── _form.html.slim  # 新規作成と編集のための共通フォームのテンプレート
└── assets/
    └── stylesheets/
        ├── users.scss 
        └── scaffolds.scss

config/
└── routes.rb  # 「resources :users」が追記されます

db/
├── migrate/
│   └── [timestamp]_create_users.rb  

test/
├── models/
│   └── user_test.rb  
├── controllers/
│   └── users_controller_test.rb  
├── system/
│   └── users_test.rb 
└── fixtures/
    └── users.yml
 

scaffoldは、足場という意味みたいです。

名前がアンダースコア(_)で始まるファイル

_test.html.slimのように名前がアンダースコア(_)で始まるファイルは部分テンプレートとして扱われます。これは他のビューファイルから簡単に再利用できるようにするための慣習で、renderメソッドを使用すると部分テンプレートを呼び出すことができます。

resources :users

UsersControllerに対するルーティングを定義しています。これにより、以下のようなルーティングが自動的に設定されます。(LaravelのRoute::resourceと似ていますね)

メソッド URL ルート名 HTTPメソッド
index /users users#index GET
new /users/new users#new GET
create /users users#create POST
show /users/{id} users#show GET
edit /users/{id}/edit users#edit GET
update /users/{id} users#update PUT/PATCH
destroy /users/{id} users#destroy DELETE

ビュー自動レンダリングについて

Railsでは、HTTPメソッドとルーティングが指定されたアクションに一致する場合、そのアクション名に基づいて対応する同名のビューファイルが自動的にレンダリングされます。

特定のアクションに対するビューレンダリングの明示的な記述がコントローラーになくても、自動的に関連付けられたビューがレスポンスされます。

ビューファイルの内容

app/views/users/index.html.slim
h1 Listing users

table
  thead
    tr
      th Name
      th Email
      th
      th
      th

  tbody
    - @users.each do |user|
      tr
        td = user.name
        td = user.email
        td = link_to "Show", user
        td = link_to "Edit", edit_user_path(user)
        td = link_to "Destroy", user, data: {confirm: "本当に削除しますか?"}, method: :delete

br

= link_to "New User", new_user_path

showメソッドへのリンクが生成されます。URLは、/users/{id(user.id)}です。

edit_user_path(user)

名前付きルートです。users#editのことです。
また今回は引数にuserがあるため、URLは/users/{id}/editです。

new_user_path

名前付きルートです。users#newのことです。URLは、/users/newです。

users_path

名前付きルートです。users#indexのことです。URLは、/usersです。

app/views/users/_form.html.slim
= form_for @user do |f|
  - if @user.errors.any?
    #error_explanation
      h2 = "#{pluralize(@user.errors.count, "error")} prohibited this user from being saved:"
      ul
        - @user.errors.full_messages.each do |message|
          li = message

  .field
    = f.label :name
    = f.text_field :name
  .field
    = f.label :email
    = f.text_field :email
  .actions = f.submit

form_for @user do |f|: @user

form_forは<form>を生成しています。 f`はフォームビルダーオブジェクトと呼ばれ、このオブジェクトを通じてフォームの入力フィールドを簡単に作成することができます。

pluralize

最初の引数に整数があると、それに基づいて2番目の引数である英単語が複数形に変化したものが渡されます。

singlarize

pluralizeの他に、singlarizeは指定した文字列を複数形に変換します。
https://railsguides.jp/active_support_core_extensions.html#pluralize

@user.errors.full_messages.each do |message|

eachメソッドは配列の各要素を取り出してmessageに格納しています。

ブロック変数

Rubyのブロック(do...endや {...} で囲まれたコード)では、ブロック内で使う変数をパイプ (| |) で囲んで宣言します。これをブロック変数と呼びます。

app/views/users/show.html.slim
p#notice = notice

p
  strong Name:
  = @user.name

p
  strong Email:
  = @user.email

= link_to "Edit", edit_user_path(@user)
'|
= link_to "Back", users_path

= notice

flash[:notice]に格納された内容を直接参照し、それを出力してフラッシュメッセージを表示しています。
alertもあります。

app/views/users/edit.html.slim
h1 Editing user

= render "form"

= link_to "Show", @user
'|
= link_to "Back", users_path

render

他のテンプレートを呼び出してその場所に内容を埋め込むことができます。

コントローラーの内容

長いので一部省略して記述します。
Railsでは、コントローラのアクション内でインスタンス変数(@で始まる変数)を設定することにより、自動的に変数をビューで利用できるようになります。

app/controllers/users_controller.rb
  # POST /users or /users.json
  def create
    @user = User.new(user_params) # user_paramsから取得したデータを元にモデルをインスタンス化

    respond_to do |format|
      if @user.save # ここでバリデーションが実行される
        format.html { redirect_to user_url(@user), notice: "User was successfully created." }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

respond_to

同一のアクションから条件に応じて異なるフォーマット(HTML、JSONなど)を同時にレスポンス処理することが可能です。

render

単一フォーマットのレスポンスしか必要としない場合に使用します。

コントローラー内で明示的に指定されたrenderメソッドがある場合、明示的に指定されたrenderメソッドが優先されてビューの自動レンダリングはオーバーライドされます。

app/controllers/⚪︎⚪︎_controller.rb
# app/controllers/⚪︎⚪︎_controller.rb
def show
  @user = User.find(params[:id])
  if @user.active?
    flash.now[:notice] = "アクティブなユーザーです!"
    render :show_active
  else
    flash.now[:alert] = "非アクティブなユーザーです。"
    render :show_inactive
  end
end

flash[:notice]

フラッシュメッセージ機能を利用して、任意のキーに対する値を取得することができます。
今回はflashハッシュの:noticeキーを直接参照していますが、flash[:error]flash[:success]などように適用できます。

app/views/users/show.html.slim
- if flash[:notice]
  p = flash[:notice]

format.html

HTMLビューをクライアントに送信します。

notice

一時的なメッセージをユーザーに表示するために使用されるフラッシュメッセージです。

format.json

JSONレスポンスを指定します。

下記のファイルが自動で追加されます。

 db/
 └ development.sqlite3 # 開発環境で使用されるSQLite3データベースファイル  
 └ schema.rb  # データベースのスキーマ情報を記述されているファイル

usersテーブルが追加され、name、emailとtimestampsが自動でマイギュレーションファイルに追記されるのでcreated_atupdated_atのカラムも自動的に作成されます。

テーブル名はモデルの複数形になります。

モデルの設定

バリデーションなどを追加してください。

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end

バリデーション

https://railsguides.jp/active_record_validations.html

presence: true

空でないこと(必須項目)

uniqueness: true

ユニークであること

with: URI::MailTo::EMAIL_REGEXP

Rubyの標準ライブラリに含まれているメールアドレスの形式を確認するための正規表現です。

ブラウザで確認

サーバーを起動してください。

bundle exec rails server

http://localhost:3000/users にアクセスして動作を確認してください。
削除が正しく機能せず、詳細が表示されると思います。

destroyではなくshowが実行される

link_tobutton_toに変更すると、正しく機能するようですが、これだとdata: { confirm: "本当に削除しますか?" }の確認ダイアログが表示されませんでした。

app/views/users/index.html.slim
td = button_to "Destroy", user, method: :delete, data: { confirm: "本当に削除しますか?" }

https://qiita.com/mnmnm_37/items/c5087d9be8d79002151a
https://www.y-i.jp/entry/rails7-form-data-confirm

コンソールを確認すると、下記のエラーが出ていました。
Error: Module build failed (from ./node_modules/babel-loader/lib/index.js): Error: Cannot find package '@babel/plugin-proposal-private-methods' imported from /Users/ユーザー名/.../babel-virtual-resolve-base.js

RailsがWebpackerを導入した際に、デフォルトでyarnを使用する設定になっているので今回はyarnを使用します。(プロジェクトにpackage.jsonはあるがpackage-lock.jsonがなく、yarn.lockが存在する場合は、そのプロジェクトがyarnを使用して依存関係を管理していることを示しています。)

yarn add @babel/plugin-proposal-private-property-in-object

これで動くようになるかと思い確認すると、次はコンソールに下記のエラーが出ていました。
Uncaught Error: Module build failed (from ./node_modules/babel-loader/lib/index.js): Error: [BABEL]: --- PLACEHOLDER PACKAGE --- This @babel/plugin-proposal-private-property-in-object version is not meant to be imported. Something is importing @babel/plugin-proposal-private-property-in-object without declaring it in its dependencies (or devDependencies) in the package.json file.

こちらも追加してみます。

yarn add @babel/plugin-proposal-private-property-in-object

無事に動作するようになりました。

JavaScriptのコードを正しく変換するために必要なBabelのプラグインが不足していたために、Rails UJSが正常に動作していなかったことが原因でした。

プラグインをインストールすることで、JavaScriptが正しく変換されるようになり、Rails UJSも正常に機能するようになったため、method: :deleteを使ったリンクが正しく動作するようになって問題が解決したと思います。

Babel

ECMAScript2015以降のコードを現在のブラウザーや環境、古いブラウザーや環境で下位互換性のあるバージョンのJavaScriptに変換するために主に使用されるツールです。
https://babeljs.io/docs/

Rails UJS

RailsでJavaScriptを使用する際に、HTML要素に特定のdata属性を指定することで、Ajax操作やその他のJavaScript関連の動作を簡単に実装できるようサポートするライブラリです。

このライブラリはWebpackerとともに動作し、Railsアプリケーションの標準的な機能としてデフォルトで含まれています。特に、フォームの非同期送信やリンクでの特定のHTTPメソッドに使われます。https://www.npmjs.com/package/@rails/ujs

package.json
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "5.4.4",

method: :delete

HTMLフォームの生成時に使用するヘルパーメソッドです。これにより、通常のHTTPリクエスト(GETやPOST)とは異なるDELETEリクエストを生成することができます。

Railsコンソール

モデルを介してデータベースにクエリを発行したり、アプリケーションのメソッドを試したり、新しいデータをデータベースに追加したりすることができます。

Railsコンソールが起動してみてください。exitまたは、quitで終了することができます。

bundle exec rails console

# 省略できます
bundle exec rails c

データがある状態で、最初のユーザーを取得してみてください。

User.first

取得できているのがわかると思います。

SQLiteデータベースに接続

下記コマンドでSQLiteデータベースに接続することができます。.quitで終了することができます。

sqlite3 db/development.sqlite3

テーブル一覧を取得してみてください。

.tables

取得できていることがわかると思います。

SQLiteのGUIツール

https://zenn.dev/nenenemo/articles/8afe097b95dfe7

テストの実行

テストを実行して、全ての変更が正しく機能するかを確認してください。

bundle exec rails test

今回は2 failures1 errorsとなっているので、ログを確認して修正します。

日本語化

rails-i18nというRailsアプリケーションを多言語対応させるためのライブラリをインストールしてください。
https://github.com/svenfuchs/rails-i18n
https://railsguides.jp/i18n.html

Gemfile
gem 'rails-i18n'

インストールしてください。

bundle install

config.i18n.default_locale = :jaを設定しない場合、デフォルトの言語は英語(:en)のままです。

config/application.rb
       config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    config.i18n.default_locale = :ja

    # 国際化に使用する追加のロケールファイルのパスを設定
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

再起動して確認すると、エラーメッセージが日本語になっていると思います。

エラー文の表示も日本語化します。
config/locales/en.ymlしかないと思ます。日本語用のja.ymlを作成して

touch config/locales/ja.yml

エラーメッセージと属性名を記述してください。

config/locales/ja.yml
ja:
  errors:
    template:
      header:
        one: '1つのエラーがあります。'
        other: '%{count}つのエラーがあります。'
  activerecord:
    models:
      user: 'ユーザー'
    attributes:
      user:
        name: '名前'
        email: 'メールアドレス'
    messages:
      record_invalid: '保存できませんでした:'

ビューファイルを修正してください。

app/views/users/_form.html.slim
= form_for @user do |f|
  - if @user.errors.any?
    #error_explanation

      h2= t('errors.template.header', count: @user.errors.count)

      ul
        - @user.errors.full_messages.each do |message|
          li = message

  .field
    = f.label :name
    = f.text_field :name
  .field
    = f.label :email
    = f.text_field :email
  .actions
    = f.submit

t

translateメソッドのエイリアスです。多言語対応させることができます。

errors.template.header

翻訳ファイル(ja.yml)に定義されている以下の箇所を指しています。

config/locales/ja.yml
ja:
  errors:
    template:
      header:
        one: '1つのエラーがあります。'
        other: '%{count}つのエラーがあります。'

count: @user.errors.count

tメソッドを使用して翻訳ファイル(ja.yml)のcount@user.errors.count(エラーの数)が渡されています。

下記のように表示されるか確認してください。

DB設定

config/database.ymlファイルでデータベース接続情報を設定しています。デフォルトではSQLiteが設定されていますが、MySQL、PostgreSQLなどに変更することもできます。

モデル作成

下記コマンドで新しいモデルを作成することができます。

bundle exec rails generate model User name:string email:string

モデル、マイギュレーションファイルなどが作成されます。

├── app
│   ├── models
│   │   └── user.rb                     
├── test
│   ├── models
│   │   └── user_test.rb               
│   └── fixtures
│       └── users.yml                  
└── db
    └── migrate
        └── [timestamp]_create_users.rb  

コントローラーの作成

アクション名を指定すると、ビューファイルなどが追加されます。
https://zenn.dev/nenenemo/articles/775be954bddb51#コントローラー作成

bundle exec rails generate controller Pages

下記のファイルが作成されます。

app/
├── controllers/
│   └── pages_controller.rb    
├── helpers/
│   └── pages_helper.rb        
├── assets/
│   └── stylesheets/
│       └── pages.scss         
└── test/
    └── controllers/
        └── pages_controller_test.rb  

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
    get 'about/index'
  get '/about', to: 'pages#about'

end

get 'about/index'

/slim/indexというパスにGETリクエストが来たときにSlimControllerindexアクションを明示的に呼び出すよう指定しています。そのため、URLは/about/indexになります。

get '/about', to: 'pages#about'

/slimというパスにGETリクエストが来たときにSlimControllerindexアクションを明示的に呼び出すよう指定しています。そのため、URLは/aboutになります。

do ... end

複数行にわたる場合

config/routes.rb
Rails.application.routes.draw do
  root "top#index" 
  resources :users 
end

{ ... }

一行で簡潔に書く小さなブロックに適しています。

config/routes.rb
Rails.application.routes.draw { root "top#index" }

ActiveRecord::PendingMigrationError

マイグレーションファイルが実行されていない(適用されていない)状態を検出したときに発生するエラーです。

今回の場合は、表示されているRun pending migrationsを押すか、表示されているようにbin/rails db:migrate RAILS_ENV=developmentを実行すると、解決します。

ActionController::UnknownFormat in UsersController#index

サーバーを起動して、http://localhost:3000/users にアクセスした際にブラウザで表示されました。

UsersController#index is missing a template for this request format and variant.
request.formats: ["text/html"]
request.variant: []

サーバーを再起動すると問題なく表示されました。

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion