はじめに
この記事はこれまでRuby on Railsを利用したWebアプリケーション開発未経験の私が実戦突入前までに取り組んだステップの概要をまとめたものです。学習をはじめた背景としては単純で、仕事でRuby on Railsで作られたサービスに携わることになったからです。
現在Ruby on Rails環境での仕事に取り組んでいるのですが、予習をしたかいがありスムーズに業務を開始することができました。
新しい環境は技術的な部分だけでなく人間関係や組織のレギュレーション等、新規に構築したり覚えなければならないことが沢山あります。なので、せめて技術的な部分だけでも事前にしっかりと準備しておきたいものです。
なので、私のようにRuby on Rails未経験の状態で新しい開発現場に臨まれる方が準備する際、今回の記事が少しでもお役に立てれば幸いと思っております。
※私自身が定期的にRailsに関する知識をオンラインでアウトプットかつアップデートすることも兼ねています。
Ruby on Railsの概要
Ruby on Rails(以下Rails)はRuby製のWebアプリケーションフレームワークです。
Webアプリケーションフレームワークとは、Webアプリケーション開発における工程を一般化した時に必要となるソフトウェアの構造、部品、設計思想をパッケージ化したものです。
アプリケーションフレームワークは言わば未完成のアプリケーションで、利用する開発者がフレームワークが用意した拡張ポイントに独自の処理を実装していくことで完成する仕組みになっています。それによって品質の高いアプリケーションを効率的に開発することができます。
設計思想
Webアプリケーションフレームワークの中心となるのは設計思想です。設計思想によって具体的なソフトウェアの構造や部品が規定されるからです。
従って、設計思想への理解はアプリケーション開発時に必要となる実装上の判断や、意思決定のコストを下げることに繋がると考えます。
Railsの場合は公式サイトのWhat is Rails?より引用すると、下記の2つが主要な原則とされています。
Don't Repeat Yourself: DRY is a principle of software development which states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". By not writing the same information over and over again, our code is more maintainable, more extensible, and less buggy.
Convention Over Configuration: Rails has opinions about the best way to do many things in a web application, and defaults to this set of conventions, rather than require that you specify minutiae through endless configuration files.
-
同じことを繰り返すな(DRY原則)
同じことを繰り返すことを否定するこの原則はソースコード上不必要な重複を避けるというコーディングの世界に閉じたものではなく、データベースのスキーマ定義、テストの実施、ビルド、ドキュメントの生成等アプリケーションが提供されるまでの工程全てに適用される包括的な考え方です。
Railsのフルスタックフレームワークという特徴は、この原則の具体化の一つでしょう。 -
設定より規約(CoC原則)
フレームワークが定めた規約に従うことで、開発者は必要最低限のコストで効率的にアプリケーションを完成させることができます。
規約に従うだけでプログラミング上の細かな意思決定や他の開発者とのコミュニケーションコストを下げられるだけでなく、実装上の開発工数も大幅に削減できます。
RubyGemsでのRailsの画面では下記のメッセージが表記されており、2つの設計原則を組み込んだWebアプリケーションフレームワークであることが明示されています。
Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.
2004年に最初のバージョンが公開されて以来、Railsが後発のフレームワークに与えた影響は多く、特にWebアプリケーション開発に用いられることが多いPHPのフレームワーク(CakePHP、Laravel)に大きく影響を与えています。
MVCフレームワーク
Webアプリケーションのフレームワーク設計においてはMVCの採用がデファクトスタンダードですが、RailsもMVCに基づいた各種コンポーネントで構成されています。
よって、他のWebアプリケーションでの開発経験があればその知識や経験はRailsでの開発にも活かせると思っています。
Ruby on Railsそのものについて更に詳しく知りたい方は、公式サイトやRuby on Rails Guidesが参考になります。
学習環境
学習は下記の環境で学習を進めていきました。
- Railsのバージョン 5.2.2
- MacBook Pro クアッドコアIntel Core i7
JET BRAINSが独自の調査をもとに公開しているThe State of Developer Ecosystem 2020のRubyの現状において、一番使われているRailsのバージョンは5.2なので多くの開発者にとって5.2を学習することは2021年3月現時点では適当な選択の一つかと思います。
CPUはApple Silicon環境でも良いのですが、Dockerを利用する関係上アーキテクチャ起因で動作が不安定になるのはRailsを学習するという目的の弊害になるので、あくまで現時点においては動作が安定しているIntelアーキテクチャ上で行います。
※この弊害は単純に時間が解決する類の問題だと認識しております。
学習を始めて最初の失敗
私はRailsに加えてRubyも初心者だったものの、プログラミング言語に関してはある程度知見があったので、横着していきなりRailsの勉強を始めようと考えていました。Railsを勉強している過程でRubyに関してわからないことが出てきた場合、その都度補完していけば良いと思っていたからです。
しかしながら、すぐにその方針を改めることになります。理由は、Rubyは他のプログラミング言語と比べてルールや内部の仕組みが違うところが意外と多いことに気づいたからです。「あのプログラミング言語ではそうだから、Rubyも多分そうはず」という感じで傲慢に思い込んだまま進むと、間違った理解が後々手痛い失敗に繋がり、結果的に体系的なRuby学習を一から始める必要が出てくると思ったからです。
よって、他のプログラミング言語の経験者であってもRuby未経験者であれば、Railsを学ぶ前に一通りRubyを体系的に学んでおくことを推奨します(当たり前の姿勢だと思いますが、個人的な反省を残しておきます)。
その姿勢に矛盾するように思えるかもしれませんが、Rubyの学習後に行うRailsの学習については「MVCフレームワークならば恐らくアレがあって、ああなる仕組みのはずだからその処理がRailsでどう実現されているのか確認してみよう」という局所的なアプローチで学習していきました。
理由は2点あります。MVCは明瞭な概念なので、そのモデルを採用しているならば、そこから逸脱した仕組みになってはいないだろうという仮説と、私自身Railsから派生したと言われているフレームワークでの開発経験があったので、そこで得た知見は適用可能だろうという仮説を持っていたからです。実際、一通りRailsを学んでみた感想としてその認識は間違っていなかったように思っています。
学習項目
業務においてRailsでWebアプリケーションの開発を行う為に必要な知識・スキルを習得する為に、重点を絞って下記の順番で確認していきました。
-
環境構築の構築
目的 : 効率的かつ実践的な学習を行う為に、Railsが動作可能であり、気兼ねなく内部の状態を改変させることが可能であり、最初から何度もやり直すことが可能な環境を構築すること -
既知のMVCのWebアプリケーションフレームワーク像をRailsにマッピングする
目的 : 既に何らかのMVCフレームワークを利用したWebアプリケーション開発に携わったことがあるという前提のもと、自分の中にあるMVCフレームワーク像をRailsに対して当てはめ、共通点と相違点を確認すること。相違点があればその認識を上書きアップデートする。 -
デバッグの方法の確認
目的 : アプリケーション新規・派生開発、不具合調査・修正作業において必要不可欠なデバッグ手法を確認すること -
テストの方法の確認
目的 : 一定の品質を保ったままリリースサイクルを重ねる為に、自動化されたテスト手法を確認すること
環境構築
以前私が書いた下記の記事を参考にDockerで環境を構築しました(もともとRailsの学習の為に下記の記事を書いたのですが)。指定したRailsのバージョンは上述した通り5.2.2です。
RailsのDocker開発環境を一撃で作成するスクリプトを作ってみました
なお、個人的観測結果になりますが、実際にサービスを提供している開発現場では色々なRubyのバージョンやRailsのバージョンが混在しているのはよくあることのようです。そうなるとやはり下記のような理由によって、環境構築はDockerによって行うべきという考えが私の中で強まりました。
- 同一のOS環境内でランタイムを柔軟に切り替えることが可能なrbenvのようなツールもあるが、Ruby以外のライブラリに依存せざるをえない問題は解消されない為、以前として依存関係の解消と整合性維持に疲弊する可能性がある
- 仮想サーバとしてOS環境ごと分離するアプローチをとることもできるが、時間的、コンピューティングリソース的にコスパがあまり良くない。また、環境の独立性だけを考えれば要件を満たすとしても、ポータビリティとしては低く現代的なアプリケーションの開発基盤としては満足とはいえない(特にWebアプリケーション開発においては)。
既知のMVCのWebアプリケーションフレームワーク像をRailsにマッピングする
次に自分が認識している「MVCのWebアプリケーションフレームワークであるならこういう作りのはずだよね」という経験から構築されたイメージをマッピングする為に、Railsで作られている簡易的なアプリケーションをそれに照らし合わせていくという方法をとりました。
イメージするフレームワークは何でも良いのですが、もしもRailsから派生したフレームワークの開発経験があるならマッピングしやすいと思います。
RailsではCRUD機能を持つWebアプリケーションの雛形のようもの(scaffold)を自動生成するscaffoldジェネレータという機能が用意されているので、これを利用しました。
なお、今後rails
というコマンドラインでの操作が頻繁に出てきますが、Railsではコマンドラインツールがアプリケーション開発・運用における幅広い支援を開発者に提供してくれます。
rails
コマンドの処理が規約に従った出力を生成してくれるので、開発者はrails
コマンドを使って操作を行うというルールに従えさえすれば、Railsの細かい規約を覚えなくてすむというメリットがあります。[参考: The Rails Command Line]
scaffoldを利用してサンプルアプリケーションを作成する
-
Railsのscaffoldジェネレータを利用して、簡易的なサンプルアプリケーション作成します。
$ bundle exec rails generate scaffold user name:string age:integer
-
マイグレーションを実行します(DBのテーブルレイアウトを作成)[マイグレーションについての参考: Active Record Migrations]
※Railsのアプリケーションにおけるテーブルレイアウト操作は、SQLを直接実行するのではなく、DSLを用いて抽象化された仕組みで実現しています。
$ bundle exec rails db:migrate
-
ルーティングを確認します
$ bundle exec rails routes Prefix Verb URI Pattern Controller#Action users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
たったこれだけの手順でWebアプリケーション(必要最低限だとしても)が完成してしまうなんて驚きです。
scaffoldジェネレータについて更に詳しく知りたい方はGetting Up and Running Quickly with Scaffoldingが参考になります。
サンプルアプリケーションを通して表示、作成、更新、削除操作を行ってみる
本当にWebアプリケーションが完成しているのか、ブラウザ上で実際に操作を行い必要最低限のCRUDができることをユーザー目線で確認してみました。
まずはRailsのサーバを起動するコマンドを実行します。これによって指定したポート番号でRailsアプリケーションがクライアントのリクエストを待ち受ける状態になります。
$ bundle exec rails s -p 3000
一通り触ってみて、どうやら本当にWebアプリケーションが完成できているらしいことがわかって感動しました。次は、開発者目線で内部実装を確認していきます。
サンプルアプリケーションにおいて表示、作成、更新、削除処理を行っているファイルを確認する
MVCのWebアプリケーションフレームワークという前提から、ルーター、コントローラ、モデル、ビューに相当するコンポーネントが存在するはずという仮説のもの確認していきます。
※フレームワークによってはルーターとコントローラに明確な境界がないものもあります
なお、下記では省略していますが実際にそのファイルが処理されているのか適当にデバッグ用のコードを挿入して、確認も行いました。あるいは、既に書いてあるものを消して変化を確認するというアプローチ等もとってみました。
-
ルーター
config/routes.rbRails.application.routes.draw do resources :users # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end
ルーティングが定義されているファイルは
config/routes.rb
でした。resourcesメソッドでルート設定を行うことで、RESTfulなインターフェースをまとめて提供する内部設定(パスとコントローラのアクションとの紐付け)が自動的に行われていました。
ルーターについて更に詳しく知りたい方は、Rails Routing from the Outside Inが参考になります。
-
コントローラ
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :set_user, only: %i[ show edit update destroy ] # GET /users or /users.json def index @users = User.all end # GET /users/1 or /users/1.json def show end # GET /users/new def new @user = User.new end # GET /users/1/edit def edit end # POST /users or /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to @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 # PATCH/PUT /users/1 or /users/1.json def update respond_to do |format| if @user.update(user_params) format.html { redirect_to @user, notice: "User was successfully updated." } format.json { render :show, status: :ok, location: @user } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 or /users/1.json def destroy @user.destroy respond_to do |format| format.html { redirect_to users_url, notice: "User was successfully destroyed." } format.json { head :no_content } end end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Only allow a list of trusted parameters through. def user_params params.require(:user).permit(:name, :age) end end
コントローラのファイルは
app/controllers
以下に格納されていました。rails routes
コマンドでルーティングを確認した時、Verb
とURI Pattern
に対してController#Action
が対応していたので、HTTPメソッドとURLパスのペアに対応するアクションメソッドがわかりました。例えばGETメソッドでusersというパスにアクセスがあった時は、index
メソッドが実行されます。なお、対応はscaffoldジェネレータで生成された上記ソースコードのコメントにも記載されています。before_action
メソッドはアクション先に処理が遷移される前に実行される処理です。これによってインスタンス変数@user
に値が設定されます。ただし、上記ではonly
オプションによって特定のアクション実行前に限定しています。また、ここでは他にStrong Parametersというユーザーの入力を安全に受け入れる機能も使われています。ファイル名についてはCoC原則のもと、この場合は
users_controller.rb
であることを強要しているようです。コントローラについて更に詳しく知りたい方は、Action Controller Overviewが参考になります。
-
モデル
app/models/user.rbclass User < ApplicationRecord end
モデルのファイルは
app/models
以下に格納されていました。scaffoldジェネレータで単純に生成した場合、モデル内は空のクラスだけ定義されていました。MVCにおいてModelはビジネスロジックを担当しますが、この場合はスーパークラスとなるApplicationRecordクラスによって必要な処理は実装されているという見方ができます。
なお、MVCにおけるModelは本来データアクセスとは独立した概念ですがRailsではデータベース処理と紐付いたオブジェクトになっているようです。※確認したところデータベースと独立したモデルを定義する方法も提供されていました。また、モデルクラスはO/Rマッパーとなっていてデータベースに対して透過的なアクセスを提供しています。これはActive Recordというモジュールによって実現されており、データベースに対する操作を高度に抽象化するインターフェースを実装しています。Railsを利用したことがない方でも名前くらいは聞いたことがあるのではないでしょうか?(私もその一人でした😅)
コントローラで実行されていたUser.all
やUser.find
もActive Recordが提供しているメソッドの一種であり、SQLを記述することなく直感的にデータベースを操作できます。ファイル名についてはCoC原則のもと、この場合は
user.rb
であることを強要しているようです。モデルについて更に詳しく知りたい方は、Active Record BasicsやActive Model Basicsが参考になります。
-
ビュー
app/views/users/index.html.erb<p id="notice"><%= notice %></p> <h1>Users</h1> <table> <thead> <tr> <th>Name</th> <th>Age</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.age %></td> <td><%= link_to 'Show', user %></td> <td><%= link_to 'Edit', edit_user_path(user) %></td> <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <br> <%= link_to 'New User', new_user_path %>
ビューのファイルはviews
以下に格納されます。Railsの場合、ビューのファイル構成はコントローラに従属するかたちで規定されているので、この場合はusers
というサブディレクトリをはさむことによってUsersController
から指示されるビューであることを表現しています。
ファイル名に関して上記の場合index.html.erb
となっていますが、これはindex
アクションからERBに対してHTML形式での出力生成を指示するビューファイルという意味です。言うまでもなくビューも他のコンポーネントと同様にCoC原則が適用されています。
ERBはRuby実装の文書埋め込みライブラリで、RailsにおいてはHTMLやプレーンテキストの生成に利用されているようです。他にもindex.json.jbuilder
等がありますがこれはJSON生成において適したビューファイルです。クライアントとサーバで責任を完全に分断してアプリケーション開発を行っているチームであれば、ERBよりもこちらを利用する方が多いかもしれません。
また、上記ファイル内ではlink_to
という汎用メソッドや、edit_user_path
というようなusersのビューに特化したようなメソッドが呼び出されています。これはビューヘルパーというビューにおいて便利な処理を実装したメソッドの集合の一部で、編集画面のURLを出力します。ビューヘルパーを提供している目的としては、RailsにおいてDRY原則を実現する為と考えられます。
ビューについて更に詳しく知りたい方は、Action View Overviewが参考にあります。
バリデーション処理
scaffoldジェネレータで生成されたコードでは、バリデーション処理について確認できませんでした。WEBアプリケーション開発においてバリデーション処理の実装は必要不可欠になりますので、独自にバリデーション処理を追加することによってその方法を学ぶことにしました。
調べたところRailsのバリデーション処理はActiveModelのValidationsモジュールを利用することによって実現していました。
RailsのModelクラス内でバリデーション使える仕組みは、最終的にActiveModelのValidationsモジュールをincludeしているからなんだね
— 親方 (@tajima_taso) February 26, 2021
method(:validates).source_locationを使ったらメソッドの定義場所見つけられるの便利 pic.twitter.com/LAqrz4UM83
空だったモデルクラス内でvalidates
メソッドを呼び出して下記のように使用してみます。
class User < ApplicationRecord
validates :age,
:numericality => { :greater_than => 10 }
end
この処理によってユーザーから8という入力を受け付けなくなりました。
11を入力してみると、受け付けることができました。
規約に従った結果、開発者が独自にロジックを実装しなくても必要なバリデーション処理が実行され、またもや感動しました。
この他にもWebアプリケーション要件に必要となるバリデーションヘルパーがあらかじめ定義されています。また、既存のヘルパーで必要なバリデーションが実現できない場合は自作することも可能です。
ちなみにバリデーションはデータベースの保存直前に実行されていることが、コントローラを下記のように修正することでわかります。
def update
respond_to do |format|
format.html { redirect_to @user, notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: @user }
#if @user.update(user_params)
# format.html { redirect_to @user, notice: "User was successfully updated." }
# format.json { render :show, status: :ok, location: @user }
#else
# format.html { render :edit, status: :unprocessable_entity }
# format.json { render json: @user.errors, status: :unprocessable_entity }
#end
end
end
どうやら@user.update
の実行時(データベースへの書き込み時)に入力値のバリデーションを行っているようです(バリデーションに失敗した場合はデータベースへの保存も行いません)。
セキュリティの観点で入力値の検証が必要かどうかについては議論が分かれるところですが、データベース保存直前より早期にバリデーションを発動させたいケースはあるかもしれません。
その場合、下記のように@user.invalid?
(あるいは@user.valid?
)を実行することによってデータベースの保存処理とは無関係に入力値の検証を行うことができます。
# POST /users or /users.json
def create
@user = User.new(user_params)
respond_to do |format|
if @user.invalid?
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
if @user.save
format.html { redirect_to @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
これは私にとっては意外な拡張性でした。Railsは規約にガチガチに従わないといけないイメージがあったからです。そこで、しっかり調べてみると、上記のようにスタンダードな規約に従わなかった場合の回避策というのがほとんどのケースにおいて用意されています。
どうしてもRailsのスタンダードな規約に従うことが難しい事態が発生した場合は、しっかりと調べれば回避策で乗り切れそうです(とはいえ、基本方針としては規約に従うべきですね)。
バリデーションについて更に詳しく知りたい方は、Active Record Validationsが参考になります。
複数のテーブルの操作
単一のテーブルアクセスを必要とするビジネスロジックの実装については、この段階ではある程度イメージができています。
しかしながら、データベースの正規化に基づいてテーブル設計を行ったWebアプリケーション開発においては、複数のテーブルを結合した上でデータを取得するケースが普通です。なので、Railsのお作法的にテーブル同士を結合したデータを取得する処理についても確認しました。
scaffoldジェネレータを用いて新しくarticleという名前でリソースを作成します。この時生成されるarticlesテーブルに対してusersテーブルを参照する外部キーの設定を行うことにします。
外部キー制約の定義に関しても、RailsのCoC原則に基づき一定のルールで行われる為、下記のような単純なオペレーションでテーブル同士の関連付けが可能です。
$ bundle exec rails generate scaffold article user:references title:string content:text
$ bundle exec rails db:migrate
新規作成のURLにアクセスして、記事(article)を作成します。Userの項目は存在しているusersテーブルのidを直接指定します。
この時、usersテーブルとarticlesテーブルのレコードは1:n(0以上)の関係になっているので、モデルとなるArticleクラス内でbelongs_to :user
を実行させることでusersテーブルのレコードに対して透過的なアクセスが可能になります。※今回はscaffoldジェネレータによって既に記述されていたのですが。
class Article < ApplicationRecord
belongs_to :user
end
ここでarticleの一覧画面を見ていると、オブジェクトを無理やり文字列表現に変換したような値が表示されています。つまりこれは、参照先となるusersテーブルの情報が取得できていると予想できます。確認の為に上記のbelongs_to
を削除してみるとエラーになるので、このメソッドの実行によってusersテーブルへの参照が実現できていることは確かです。
続いてビューのファイルを確認します。名前を表示するように修正してみたのが下記です。
<p id="notice"><%= notice %></p>
<h1>Articles</h1>
<table>
<thead>
<tr>
<th>User</th>
<th>Title</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @articles.each do |article| %>
<tr>
<td><%= article.user.name %></td>
<td><%= article.title %></td>
<td><%= article.content %></td>
<td><%= link_to 'Show', article %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Article', new_article_path %>
article.user.name
の箇所はもともとarticle.user
になっていました。article.user
はオブジェクトなので、オブジェクトを無理やり文字列表現したような出力になっていたようです。
修正後の表示は下記になりました。User名が想定通りの出力になっています。
今度は逆に、参照先から参照元のデータにアクセスする為にはhas_many
メソッドを実行する必要があります。
class User < ApplicationRecord
has_many :articles
validates :age,
numericality: { greater_than: 10 },
presence: true
end
ビューでは下記のようなループ処理をさせてみます。
<p id="notice"><%= notice %></p>
<h1>Users</h1>
<%#= method(:edit_user_path).source_location %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= user.name %></td>
<td><%= user.age %></td>
<td><%= link_to 'Show', user %></td>
<td><%= link_to 'Edit', edit_user_path(user) %></td>
<td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<td>
<% user.articles.each do |article| %>
<%= article.title %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New User', new_user_path %>
こういったActive Recordのモデル同士を関連付けることにより、テーブル同士のデータの関連付けを抽象する仕組みをRailsではアソシエーションと呼ぶようです。
Active Recordを利用したデータアクセスの抽象化インターフェースはとても便利ですが、データ量を考慮せずに使うと深刻なパフォーマンス問題を引き起こしそうです。
たとえば上記のコードはN+1クエリ問題を起こしてます。まだデータ量が少ないので体感的には何も問題を感じませんが、ユーザー1人に対して記事がたくさん関連づけられるようにデータが蓄積されていく場合、SQLの発行回数や計算量が指数関数的に増大していくことが想定されます。
幸いこういったRailsにありがちなパフォーマンス劣化問題に対する対処法は、ネットで調べるといくらでも出てくるので(includes
やjoins
メソッド)、開発者はそれらを参考にして積極的に対処していく姿勢が求められそうです。ちなみに公式ではEager Loading Associationsという手法が紹介されていました。
アソシエーションについて更に詳しく知りたい方は、Active Record Associationsが参考になります。
SQLの直書き
前述のように、Railsにおいてはデータベースに対する操作はActive Recordによって抽象化されたインターフェースによって行うのが王道です。ただ、そうはいっても複雑なテーブル結合が必要なビジネスロジック実現においては、SQLを直接記述した方が拡張性や明快さ、あるいはパフォーマンスの面で優位性が高いケースもあると思います。
そういったニーズにもActive Recordは対応しているので開発者としては安心です。たとえば、User.find
を下記のようにSQLを直書きするメソッドに書き換えても結果は同じです。
def set_user
#@user = User.find(params[:id])
@user = User.find_by_sql( ['SELECT `users`.* FROM `users` WHERE `users`.`id` = ? LIMIT 1', params[:id]]).first
end
上記含め他にもActive Recordを利用したデータ取得方法は色々あるので、Active Record Query Interfaceが参考になります
デバッグの方法
デバッグは一般的にバグの認識から修正までの一連の作業を指しますが、ここではプログラムの処理中、内部の状態がどうなっているのか?をRailsにおいて分析する手段について確認します。
よって、この手段はバグ修正以外の開発時にも有効です。具体的には、変数の中身を開発者が確認するにはどうするのか?等の手法ということです。
この手法を活用すれば、例えば前述のN+1クエリ問題に関しても、開発時の段階で潜在的な問題に気づくことができると思います。
なお、IDEによっては独自の支援機能により生産性の向上に寄与する機能が提供されていることもありますが、今回は一般的な環境でデバッグに使えそうな方法にフォーカスします。
Railsサーバのログ
railsコマンドでサーバを起動すると、起動した端末のコンソール上に実行中の処理に関わる様々な情報が出力されます。その中には発行されたSQLも含まれます。また、結果はlog/development.log
にもログとして残ります。これだけでもかなり便利なので、「勝った!」と思いました。
logger.debug
サーバのコンソール上に独自の出力を行いたい場合は、logger.debug
メソッドを利用できます。puts
やprint
等でも出力が可能ですが、その場合はログとしては残らなかったので永続的に残すにはこの手法が有効だと思います。
logger.debug 'hoge'
inspectメソッド
最も手軽にオブジェクトの中身を確認する手法です。レシーバのオブジェクトの中身をヒューマンリーダブルな文字列に変換するRuby標準メソッドです。
Rails以前のRubyの学習の段階でこれは使えるなと思っていました。logger.debug
等と組み合わせて使うケースが多いのではないでしょうか?
# GET /users or /users.json
def index
@users = User.all
logger.debug @users.inspect
end
Byebug
デバッガと聞いて一般的に思い浮かべるのは、GDBやLLDBのようなブレークポイントを追加してステップ実行を行うようなツールではないでしょうか?ここでは私が気に入ったByebugを紹介します。
ByebugはRubyプログラムに対してそのような機能を提供するデバッガの一つですが、Railsでも使えました。
# インストールされているか確認する
$ gem list -i byebug
true
使い方は簡単です。デバッグした箇所にbyebug
を追加します。
# GET /users or /users.json
def index
byebug
@users = User.all
end
追加した箇所で一旦処理がストップし、サーバを起動した端末内でユーザーの入力待ちになります。後は一般的なデバッガで提供されているような命令でデバッグ可能です。
どのような命令が用意されているのかはhelp
コマンドで一覧化されます。
(byebug) help
ちなみに、上記環境構築のようにdocker-compose
を利用してByebugを使用する場合は、コンテナ起動時に疑似TTYを割り当て、標準入力を開いたままにしておきます。その後、バックグランドで起動しているコンテナに対してアタッチを行うというアプローチが良いでしょう。ちなみに、docker-compose.ymlと実行するコマンドだけ記載すると下記です。
stdin_open: true
tty: true
$ docker-compose up -d && docker container ls
Starting rails_db_1 ... done
Starting rails_web_1 ... done
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1cd0b6b9bf2b rails_web "entrypoint.sh bash …" 6 minutes ago Up Less than a second 0.0.0.0:3000->3000/tcp rails_web_1
1d242dd32992 mysql "docker-entrypoint.s…" 6 minutes ago Up 2 seconds 3306/tcp, 33060/tcp rails_db_1
$ docker attach 1cd0b6b9bf2b
Started GET "/users" for 172.23.0.1 at 2021-02-28 09:32:50 +0000
Cannot render console from 172.23.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
(0.5ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
↳ /usr/local/bundle/gems/activerecord-5.2.2/lib/active_record/log_subscriber.rb:98
(13.0ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
↳ /usr/local/bundle/gems/activerecord-5.2.2/lib/active_record/log_subscriber.rb:98
Processing by UsersController#index as HTML
[3, 12] in /myapp/app/controllers/users_controller.rb
3: before_action :set_user, only: %i[ show edit update destroy ]
4:
5: # GET /users or /users.json
6: def index
7: byebug
=> 8: @users = User.all
9: end
10:
11: # GET /users/1 or /users/1.json
12: def show
(byebug)
デバッグには他にも色々なアプローチがあります。気になる方は、Debugging Rails Applicationsが参考になると思います。
テストの方法
Railsではテストを下記の4つに分類しているように思いました。下にいくほどテストにともなう工数は膨らんでいきます。
- 単体テスト(unit test)
- コントローラ内部で利用されるコンポーネント(モデル、ビュー)内の動作確認
- 機能テスト(functional test)
- 単一のコントローラに含まれるアクションの動作確認
- 結合テスト(integration test)
- 実際のワークフローに基づいた複数のコントローラにまたがる動作確認
- システムテスト(system test)
- UI(ブラウザ)の挙動を考慮した最も実稼働環境に近い想定での動作確認
組織やチーム、あるいはそのフェーズによってテストに関する考え方は多様だと思うので、テストカバレッジの方針をどうするかは異なります。ですが、開発者として単体テストくらいは言われなくても書くくらいの主体性は持っておいても損はないと思っています。
では、何のテストフレームワークを用いてテストの手法を知るべきか?ということになるんですが、Railsでは標準でMinitestというテストフレームワークを採用しています。しかし、調べてみると現実によく使われているテストフレームワークはRSpecというものらしいことがわかりました。
MinitestよりもRSpecが利用されているからには、Minitestでテストすることに対して辛みがあると思うのですが、正直Rails開発初心者の私はまだそれを実感してはいません(辛みを感じている人の見解はいくつか目を通して頭では理解しています)。ただ一定のシェアを占めているのであれば、RSpecを理解しておくメリットは大きいと思うので今回はRSpecに関して確認してみます。
RSpecは標準ではインストールされないので、rspec-railsを参考にインストールおよびセットアップを行いました。インストール直後はまだ何もテストされないですが、実行は可能です。
$ bundle exec rspec
No examples found.
Finished in 0.00035 seconds (files took 0.08257 seconds to load)
0 examples, 0 failures
ユニットテスト
モデルのユニットテストを行う準備として、下記のコマンドでRspecでテストする為のボイラープレートが生成できました
$ bundle exec rails generate rspec:model user
生成されたボイラープレートを編集するのですが、ユーザーのageの入力が11の時は検証に成功し、10の時は失敗するテストは下記のようなコードで実施できます。この時、三角測量等の手法を使ってテストが正しく動作していることも確認した方が良いですね。
require 'rails_helper'
RSpec.describe User, type: :model do
it "is valid age" do
user = User.new(
age: 11
)
expect(user).to be_valid
end
it "is invalid age" do
user = User.new(
age: 10
)
expect(user).to be_invalid
end
end
$ bundle exec rspec
Finished in 0.06465 seconds (files took 1.17 seconds to load)
2 examples, 0 failures
RSpecは多機能なテストフレームワークですが、基本的にはexpect
メソッドとmatcherと呼ばれる評価メソッドを組み合わせることでアサーションを実現しているようです。[参考: Built in matchers]
ちなみにbe_xxx
という形式のmatcherはPredicate matchersといい、actual.xxx?
という書式のメソッドが実行可能な時、expect(actual).to be_xxx
という判定が行うことができるmatcherのようです(知っていると便利ですが、知らないと困惑しそうですね 😇)。
RSpecのAPIリファレンスについてはAPI Documentationを、Railsのテストそのものについて更に詳しい情報はTesting Rails Applicationsが参考になります。
まとめ
Railsでのアプリケーション開発未経験の私が、Railsの概要を把握する為に行ったステップについて共有させて頂きました。もちろん、これだけの知識ではまだまだ十分ではないのは承知していますが全体像はある程度掴めたと思います。また、更に特定の領域を深堀りしたくなったらどこを起点にしてどこを向けば良いのかも把握できました。
個人的には環境構築面での辛みを最も感じたので、今後サービス開発に関わっていく中でCI/CD含めよりよい方法を模索していきたいと思います。
何か新しいことにチャレンジする時、不安に思うのは、最初にどこを起点にして進めていけば良いのかわからないこと、全体感がわからないこと、そして全体の中で今自分はどの位置にいるのかわからないことだと思っています。今回、そこは自分の中で掴むことができたので今後は自分の立ち位置と必要な部分との差分を埋めるように知識やスキルを拡張していきたいと思います。