Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active June 16, 2020 01:48
Show Gist options
  • Save yano3nora/3dadc56b5970a23a0528d9d4066b2110 to your computer and use it in GitHub Desktop.
Save yano3nora/3dadc56b5970a23a0528d9d4066b2110 to your computer and use it in GitHub Desktop.
[rails: note] Ruby on Rails - MVC web application frame work by ruby. #ruby #rails

OVERVIEW

rubyonrails.org
rails/rails - github.com

Ruby 製の MVC WEB アプリケーションフレームワーク。アジャイル開発に適しスタートアップ系企業の採用例が多く、クックパッドや食べログなどでも利用されている。「設定より規約」で DRY 重視。RESTful なインターフェイスや HTML 5 / モダン JavaScript への追従も早く WEB の流行をいち早く取り込んでいて、バージョンアップは割と早めなのでメジャーバージョンの移行は結構大変みたい。

Design Pattern

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)
中規模Web開発のためのMVC分割とレイヤアーキテクチャ
Railsで導入してよかったデザインパターンと各クラスの役割について

Rails は基本的には「シンプルな MVC パターン」を提供しており、Laravel のように「いろんなデザインパターンに使えるクラスは俺が用意しておいたぜ」って感じではない。また結構歴史が古いため Rails 開発者間では「あって当然」な各種デザインパターン系クラスが存在することが多いみたい。基本の MVC や Rails が ( 多くは ActiveSupport で ) 提供する Concern など以外も把握しておくこと。また、実際には Uploader とかシステムの Feature に応じたわかりやすい名前のクラスも app 下でクラス化されていることが多い。

# Base
- ActiveModel
- ActionController
    - Helpers
- ActionView
    - FormHelper
        - FormBuilder ヘルパーにより生成されるインスタンスで拡張も可能
- ActionMailer
- ActiveResource 外部 API やサービス連携
- ActiveJob 非同期処理やジョブ実行

# ActiveSupport
- Concern
    - Model / Controller の共通処理 ( 関心 ) 共通化のためのモジュールで鬼よく使う

# Design Pattern Classes
- Assert
    - フロントエンドのアサーションに利用するらしい
* Decorator
    - Model の状態に応じた View ロジックや文字列フォーマットに利用
    - 一般的に MVC パターンで Fat Model を避けるための ViewModel レイヤとして非常によく利用される
    - 単一 Model や単一インスタンスに対するロジック ... という文脈が多いみたい
    - draper gem で実装することが多いが同じくサードパーティーの active_decorator とかいう紛らわしい gem もある
- Exception
    - よくある独自例外つかいたいときのやつ
    - raise 'hoge' で RuntimeError でおk ... ってとき以外に 
- Factory
    - いわゆるファクトリパターンで以下のようなインスタンス生成に利用
        - 複数オブジェクトから単一オブジェクトを生成
        - 非構造化データからデータオブジェクトを生成 CSV → DataFrame
        - 共通インタフェースを持つオブジェクトの透過的生成
* Form
    - ある Model がいくつかのフォームに依るロジックを持つ場合に便利なやつ
    - パラメータ毎のバリデーションや異常系処理などを詰め込むことが多いみたい
    - Rails 的に言うと FormBuilder からの受け処理全般を保持することが多い
        - Model インスタンスを引数に new され params を引数に build され valid? とか聞かれる感じ
            - Valid 時の永続化トランザクションもそのまま内部に持ったりすることも結構あるみたい
        - メソッドとしては create や update で登場することになる
    - 後述する ViewObject が無いパターンでは ViewObject として new や edit で View に Passing したいデータや入力項目など「フォーム準備」の役割を担うこともある
- Job/Worker
    - ActiveJob や Sidekiq による非同期処理を担当させるクラス
- Notifier
    - 通知系おまとめ ActionMailer をラップすることも
- Presenter
    - 複数 Model を跨ぐような複雑な View ロジックを担当させる ViewModel レイヤ
- Repository
    - データアクセス抽象化クラス、鬼複雑な検索処理とか扱う場合に。Concerns で Module として実装して Model から include するのがいい?
- Resource
    - ActiveResource をラップするときにつかうとか?
* Service
    - 他クラスで吸収できない「ビジネスロジック」特化のクラス
- Task
    - Rake タスクで実行するやつのラッパ作ったり?
- Util
    - 専門性の低い雑多な共通処理を扱うやつ
- Validator
    - 独自バリデーション詰め込みクラス
* ViewObject
    - Form と混同しやすいが View に Passing したいデータに関する責務持ちクラス
    - 基本的には所謂 ViewModel レイヤとして View にロジックを入れたくなるようなケースで利用
    - Rails 的に言うと `@hoge` など View へ渡すデータをどんな風に Build するかを管理する
        - つまりメソッドとしては new や edit で登場することになる

Install

新規Railsプロジェクトの作成手順まとめ
Bundlerの使い方
ライブラリ? gem? bundler? -- Rubyのgem管理に関するあれこれまとめ

Rails は gem なので Ruby の gem パッケージ管理ライブラリ Bundler で Rails 本体、および依存パッケージ群をプロジェクトローカルへインストールし、プロジェクト毎に管理するのが一般的。因みに Bundler も gem なので、こいつだけはグローバルにぶち込む。

# Ruby 実行環境 ( ruby や gem コマンド実行可能 ) 前提
# Bundler のグローバルインストール
#
$ gem install bundler

# プロジェクトディレクトリの作成
#
$ mkdir my_project
$ cd my_project

# Gemfile に依存パッケージを記載
# ( まずは ruby と rails 本体 )
#
$ vi Gemfile
> source 'https://rubygems.org'
> git_source(:github) { |repo| "https://github.com/#{repo}.git" }
> 
> ruby '2.5.3'  # このプロジェクトで使う ruby のバージョン
> 
> gem 'rails', '~> 5.2.2'  # このプロジェクトで使う rails バージョン

# Bundler で rails gem をローカルインストール
# `--path` なしだとグローバルインストール扱いになる
#
# Bundler 経由でローカルインストールした gem のバイナリを実行するときは
# 通常 bundler exec 経由で実行しないと PATH 解決できないが、これを省略するため
# --binstubs で bin を指定ディレクトリにまとめ export で PATH を通している
# Docker 環境ならコンテナ毎に隔離されているのでこの --path --binstubs は不要
#
$ bundle install --path=vendor/bundle --binstubs=vendor/bin
$ export PATH=./vendor/bin:$PATH

# Rails アプリケーションを新規作成し FW スケルトンを生成
# Rails の依存パッケージが Gemfile に追加されるため再度 bundle install
# 一度 bundle install --path したら次から --path は省略してもいいみたい
# ちなみにここで `--api` にすると Rails が API モードに ( ビューなし )
#
$ rails new .
$ bundle install --path vendor/bundle --binstubs=vendor/bin

# DB 設定 / 作成
$ vi config/database.yml
$ rake db:create

# 開発用サーバ起動
$ rails server

# モデル生成
$ rails generate model User email:string:uniq password:string

# アソシエーション/バリデーションをごにょごにょ
$ vi app/models/user.rb

# データ型とかインデックスとかごにょごにょ
$ vi db/migrate/20180101000000_create_users.rb

# マイグレート
$ rake db:migrate

# DB ドロップ
$ rake db:drop

# セットアップ ( db:create / db:schema:load / db:seed )
$ rake db:setup

# キャッシュ削除
$ rake tmp:cache:clear                     # ファイルだけ削除
$ bundle exec rails r 'Rails.cache.clear'  # Redis にいれた動的キャッシュも削除
# クエリキャッシュだけ削除
$ bundle exec rails r 'ActiveRecord::Base.connection.query_cache.clear'

# ログ削除
$ rake log:clear

# コンソールで確認
$ rails c
$ irb> User.all
> User Load (1.0ms)  SELECT  "users".* FROM "users" LIMIT $1  [["LIMIT", 11]]
> => #<ActiveRecord::Relation []>

# コード数などのプロファイリング
#
# https://easyramble.com/check-loc-on-rails.html
#
$ rake stats

rails server

Apache x Passenger x Rails - redmine.jp
Rails5.2+Puma
Rails + Puma + Nginx on CentOS 7 の 環境構築メモ

Ruby アプリケーションは Apache や nginx とは別に、Ruby アプリサーバが必要。Rails 5 系からは標準で Puma という Ruby アプリサーバを兼ねた Web サーバがビルトインされており rails server で即立ち上がる。開発時はこれを利用するのが一番楽。本番環境では前段に別途 WEB サーバをかますとよろしい。

rails console

rails console
Railsのconsole機能を使ってModelの動作を確認

# 初回は起動に少しかかるから待っててね
$ rails c 
> Running via Spring preloader in process 58
> Loading development environment (Rails 5.2.1)

# 実 DB を動かしたくないときは sandbox モードで
$ rails c --sandbox

# 環境指定も可能
$ rails c -e development

# こんな感じで動かせる
pry(main)> user = User.find(1)

# View 系のヘルパーは app レシーバが必要
pry(main)> app.url_for(action: :show, controller: 'users', id: 1)
pry(main)> app.users_path(id: 1)

# 終了
pry(main)> quit

credentials.yml.enc / master.key

Rails5.2から追加された credentials.yml.enc のキホン

# Rails.application.credentials.secret_key_base の確認
$ rails c
irb> Rails.application.credentials.secret_key_base
irb> => xxxxxxx...

rails-ujs

Rails で JavaScript を使用する
rails-ujs - npmjs.com

Rails では控えめな JavaScript 連携のための JS パッケージ rails-ujs を利用することで Ajax や form_tag との JS 連携を強化することができる。5系より前は jQuery 依存だったが、5.1 系より依存を取り除いて npm パッケージになった。

$ npm i -S rails-ujs
import Rails from 'rails-ujs'
Rails.start()
<%=
  button_to 'Terminate Account', delete_user_path(@user),
  method: :delete, class: 'btn btn-danger',
  data: { confirm: 'Are you sure?' }  # こいつを動かすのに必要
%>

railsconfig/config

railsconfig/config - github.com

Rails プロジェクトでは設定ファイルや定数の取り扱いについて、別途 gem を入れてることが多いみたい。

$ vi Gemfile
> gem 'config'

$ bundle install

# インストールしつつ環境毎の設定 YAML を生成
$ rails g config:install
> config/settings.yml
> config/settings/development.yml
> config/settings/production.yml
> config/settings/test.yml

# Config 定数名で各環境の YAML にオブジェクト的にアクセス可能に
$ vi config/initializers/config.rb
> Config.setup do |config|
>   config.const_name = 'Config'
> end

Naming

ディレクトリ ファイル クラス
app/controllers/ users_controller.rb UsersController
app/models/ user.rb User
app/views/users/ index.html.erb -
app/views/layouts/ user.html.erb -
app/helpers/ users_helper.rb -
db/migrate/ 20080906120000_create_users.rb CreateUsers
test/fixtures/ user.yml -

Database

  • テーブル名は複数形 users
    • 単語区切りは _
    • 対応モデルクラス名はテーブル名アッパーキャメル
  • Primary Key カラム名は id
    • Foreign Key カラム名は テーブル名の単数_id
  • DATE 型のカラム名は 受動態_on
    • published_on
  • TIMESTAMP / DATETIME 型のカラム名は 受動態_at
    • updated_at / created_at
  • アソシエーション
    • 関連させたいテーブル名をくっつけた名前にする
      • userstokens の交差は users_tokens
    • id を作らず、関連する 2 つの Foreign Key セットを Primary key にする

Mode

Rails には 3 つのモード ( 実行環境 ) があり、WEB / アプリサーバ実行時に環境変数を設定することで切り替える。デフォルトでは development モードで立ち上がる。

  • production
    • ログレベル info
    • キャッシュ有効
    • 更新には再起動が必要
  • development
    • ログレベル debug
    • キャッシュ無効
      • VM 環境下だとがっつりキャッシュ効いちゃう 対策
    • 更新を即時反映
  • test
    • ログレベル debug
    • キャッシュ有効
    • 自動テスト用

Deploy onto Heroku as production

Settings for Heroku

$ vi Gemfile
> gem "rails_12factor", group: :production

$ bundle install

database.yml

Railsでconfig/database.ymlを使わずURL文字列でDB接続したい

Heroku などへのデプロイの際に「開発環境はローカル DB だけど本番環境 ( Heroku ) では Heroku Postgres のようなクラウドサービス」というのがよくある。Rails アプリは config/database.yml よりも環境変数 DATABASE_URL を接続先として優先する ので、割と楽に対応できる。

# 開発環境
DATABASE_URL=postgres://db  # 開発時は別途立ち上げている DB コンテナを参照

# 本番環境 ( 実際には .env に書かずに Heroku 管理画面で設定するなど )
DATABASE_URL=postgres://xxx:yyy@zzz/hoge  # 本番ではサービスの URL を参照
# host や url 指定をしなくても .env の DATABASE_URL が採用される

default: &default
  adapter: postgresql
  encoding: utf8
  username: <%= ENV.fetch('POSTGRES_USER') %>
  password: <%= ENV.fetch('POSTGRES_PASSWORD') %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
development:
  <<: *default
  database: app_development
test:
  <<: *default
  database: app_test
production:
  <<: *default
  database: app_production

Directories

/
├─ /bin - シェル用バイナリ
├─ /tmp
│ ├─ restart.txt     再起動用ファイル ( touch で再起動 )
| └─ /storage        アップロードファイル入れ
├─ /log
├─ /public            公開ファイル入れ ( favicon / html )
├─ /vendor
├─ /config
│ ├─ /environments   実行環境設定ファイル入れ
│ ├─ /locales        ロケールファイル入れ
│ ├─ application.rb  アプリのセットアップファイル
│ ├─ routes.rb       ルーティング設定ファイル
│ ├─ storage.yml     クラウドストレージサービス設定ファイル
│ └─ database.yml    DB 設定ファイル
├─ /db
│ ├─ /migrate        マイグレーションファイル入れ
│ └─ seeds.rb        シード
├─ /app
│ ├─ /assets
│ │ ├─ /javascripts JS のアセットパイプライン
│ │ └─ /stylesheets CSS のアセットパイプライン
│ ├─ /models
│ │ └─ user.rb
│ ├─ /controllers 
│ │ └─ users_controller.rb
│ ├─ /views	
│ │ ├─ /shared       共通パーシャル入れ
│ │ ├─ /layouts      レイアウト入れ
│ │ └─ /users        各コントローラ対応ビュー入れ
│ │    └─ /index.html.erb
│ └─ /helpers         コントローラ固有のビューヘルパー入れ
│   └─ users_helper.rb
└─ /test	 
  ├─ /controllers
  │ └─ users_controller_test.rb
  └─ /helpers
    └─ users_helper_test.rb

Docker や Vagrant などの VM 開発時の development 設定

Railsで「Cannot render console from Allowed networks」と言われたら
Rails5.2のdevelopment環境でControllerの変更が反映されないときは

Rails の開発用コンソールは localhost アクセスしか想定していないので Docker や VM を介してアクセスする場合はホワイトリストへ自身 IP を登録する必要がある。面倒なら開発環境限定で 0.0.0.0/0 で全許可するのがよい。また、 Rails は development 時にファイル変更イベントを探知して内部でキャッシュを削除しているみたい。だが Vagrant + VirtualBox で rsync などを利用せず同期フォルダー下で動作させているとき、ファイル変更のイベントを探知できずキャッシュが production と同様に残ってしまう。回避するためにファイルの WATCH 方法を変更してやる。

# config/environments/development.rb

# config.file_watcher = ActiveSupport::EventedFileUpdateChecker  # 殺す
config.file_watcher = ActiveSupport::FileUpdateChecker           # 追記
config.web_console.whitelisted_ips = '0.0.0.0/0'                 # 追記

環境変数を .env で管理

bkeepers/dotenv

Docker などを利用している場合は Docker 経由で .env から環境変数をセットできるが、Rails でも dotenv という gem を導入することでアプリケーションルートの .env から環境変数をロードできる。

# Gemfile
gem 'dotenv-rails'

Configurations

Railsアプリを設定する

stdlib や外部ライブラリの読み込みは個別に行っても良いが、アプリケーション全体に跨る設定などは config/application.rb や、環境毎で異なる場合は config/environments 下に書いちゃうのが良いみたい。

# config/application.rb

require 'rails/all'
require 'uri'        # 適当に使う stdlib とか読み込んどく

module App
  class Application < Rails::Application
    config.load_defaults 5.2
    config.i18n.default_locale = :en  # デフォルトロケール
    config.time_zone = 'UTC'  # Time / TimeWithZone が参照し created_at / updated_at に影響
    config.active_record.default_timezone = :utc  # DB の without timezone なカラム ( datetime 型等 ) の読み書きに影響
    config.action_controller.default_url_options = {  # URL メソッド設定
      host: ENV.fetch('DOMAIN'),
      protocol: 'https',
    }
    config.action_mailer.raise_delivery_errors = true  # メール設定
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address: ENV.fetch('SMTP_HOST'),
      port: ENV.fetch('SMTP_PORT'),
      user_name: ENV.fetch('SMTP_USERNAME'),
      password: ENV.fetch('SMTP_PASSWORD'),
      authentication: ENV.fetch('SMTP_TYPE'),
      enable_starttls_auto: ENV.fetch('SMTP_TLS'),
    }
    config.action_mailer.default_url_options = {
      host: ENV.fetch('DOMAIN'),
      protocol: 'https',
    }
  end
end

よくある lib や util のオートロード

# config/application.rb
module App
  class Application < Rails::Application
    # Rails 標準以外のデザパタ系や独自ユーティリティクラスの読み込みはこのへんでやることが多いみたい
    config.autoload_paths += %W(#{config.root}/app/services)
    config.autoload_paths += %W(#{config.root}/app/forms)
    config.autoload_paths += %W(#{config.root}/app/factories)
    config.autoload_paths += %W(#{config.root}/app/view_objects)
    config.autoload_paths += %W(#{config.root}/app/validators)
    config.autoload_paths += %W(#{config.root}/app/libraies)
    config.autoload_paths += %W(#{config.root}/app/utilities)
  end
end

名前空間 + ディレクトリによる領域分け

Rails(5)namespace でファイルを分ける方法

# config/routes.rb
namespace :admin do
  resource :user
  get '/login',  to: 'users#login'
  get '/logout', to: 'users#logout'
end

__END__

# クラス/ディレクトリ構成は以下のようになる
/app
├─ /controllers
│  ├─ application_controller.rb     # ApplicationController < ActionController::Base
│  └─ /brands
│     ├─ application_controller.rb  # Admin::ApplicationController < ApplicationController
│     └─ users_controller.rb        # Admin::UsersController < Admin::ApplicationController
└─ /views
   ├─ /layouts
   |  ├─ application.html.erb
   |  └─ /admin
   |     └─ application.html.erb    # layouts/application.html.erb からコピって OK です
   └─ /brands
      └─ /users
         ├─ login.html.erb          # layouts/admin/application.html.erb が適用される
         └─ logout.html.erb         # コントローラは Admin::UsersController が適用される
名前空間のネスト
namespace :admin do
  # ...
  namespace :api do
    # ...
  end
end

__END__

/app
├─ /controllers
   ├─ application_controller.rb     # ApplicationController < ActionController::Base
   └─ /admin
      ├─ application_controller.rb  # Admin::ApplicationController < ApplicationController
      └─ users_controller.rb        # Admin::UsersController < Admin::ApplicationController
         └─ /api
            ├─ application_controller.rb  # Admin::Api::ApplicationController < Admin::ApplicationController
            └─ users_controller.rb        # Admin::Api::UsersController < Admin::Api::ApplicationController

GENERATE

rails コマンド
いつも忘れる「Railsのgenerateコマンド」の備忘録
Railsコマンドでgenerateしたのを取り消したい場合(メモ)

CakePHP でいうところの bake で Laravel でいうところの artisan コマンド。ご利用は計画的に。

# CRUD の MVC ぜんぶ ( -p: ドライラン / --assets=false: アセット抜き )
$ rails g scaffold article title:string body:text published_at:datetime

# Scaffold モデル抜き
$ rails g scaffold_controller articles

# Scaffold モデル抜き  ( skip test / assets / helper )
$ rails g scaffold_controller articles --skip-helper --skip-assets --skip-test-framework

# モデルのみ
$ rails g model user name:string age:integer is_admin:boolean

# コントローラとビューのみ
$ rails g controller tags index add edit delete

# コントローラとビューのみ ( skip test / assets / helper )
$ rails g controller users --skip-helper --skip-assets --skip-test-framework

# コントローラとビューのみ ( 名前空間つき )
$ rails g controller admin/users

# jbuilder ( json 返却用ビュー )
$ rails g jbuilder article

# mailer ( メール本文用ビュー )
$ rails g mailer article

# task ( CLI タスク )
$ rails g task article

# マイグレーションのみ
$ rails g migration 名前 [カラム名:型]  [オプション]

# Generate したファイルの削除
$ rails destroy 生成したファイルの種類 [削除するファイル名] [オプション]

ROUTING

Rails のルーティング
リソースベースのルーティング: Railsのデフォルト
Railsのルーティングあれこれ

# 現在のルーティングを確認できる
# Prefix は redirect_to や link_to で _url や _path のように利用可能
# Prefix が root なら root_url や root_path となる
# _url は絶対パスでリダイレクトで利用する / _path は相対パスで link_to なんかで利用

$ rake routes
> Prefix Verb URI Pattern Controller#Action
> root   GET  /           users#login
> ...
# config/routes.rb

# root 指定 ( 最上位に記載 )
root 'articles#index'  # '/' で記事一覧へ

# 通常のルーティング
get '/articles/:id',     to 'articles#show'

# 引数省略可パターン
get '/articles/(:page)', to 'articles#show', defaults: {page: 1}

# as で名前付け
get '/login',  to: 'users#login',  as: :login
get '/logout', to: 'users#logout', as: :logout

# match + via
match "user/account" => "user#account", as: :user_account, via: [:get, :post]

# リソースベースルーティング
# GET     /photos           photos#index   すべての写真の一覧を表示
# GET     /photos/new       photos#new     写真を1つ作成するためのフォームを返す
# POST    /photos           photos#create  写真を1つ作成する
# GET     /photos/:id       photos#show    特定の写真を表示する
# GET     /photos/:id/edit  photos#edit    写真編集用のフォームを返す
# PUT     /photos/:id       photos#update  特定の写真を更新する
# DELETE  /photos/:id       photos#destroy 特定の写真を削除する
resources :photos, :books, :videos

# 単数形リソースルーティング
# GET     /user/new    user#new     ユーザを1つ作成するためのフォームを返す
# POST    /user        user#create  ユーザを1つ作成する
# GET     /user        user#show    1つしかないユーザを表示する
# GET     /user/edit   user#edit    ユーザ編集用のフォームを返す
# PUT     /user/       user#update  1つしかないユーザを更新する
# DELETE  /user/       user#destroy ユーザを削除する
resource :user 
get '/profile', to: 'users#show'  # /profile = users#show はログインユーザ専用となる

# リソースベースルーティング + 除外
resources :attachments, except: [:show]

# 名前空間
namespace :admin do
  resources :users, :tweets
end

# ワイルドカードとパスパラメータ
get 'books/*section/:title', to: 'books#show'  # params[:section] や params[:title] がとれる

# フォーマット ( ファイル や JSON 返却 REST API とか )
get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
defaults format: :json do
  resources :feeds
end

# リダイレクト
get '/stories', to: redirect('/articles')

as x link_to

コードからパスやURLを生成する

# config/routes.rb
get '/articles/:id', to: 'articles#show', as: 'articles'

# app/controllers/articles_controller.rb
def index
  @articles = Article.all
end

# app/views/articles/index.rb
<ul>
<% @articles&.each do |article| %>
  <li>
    <!-- :as で指定した prefix を用いた動的リンク生成 -->
    <%= link_to @article.title, article_path(@article) %>
  </li>
<% end %>
</ul>

Nesting

http://weblog.jamisbuck.org/2007/2/5/nesting-resources

リソースのネスティングは、ぜひとも1回にとどめて下さい。決して2回以上ネストするべきではありません。

Order

Railsのルーティングは、ルーティングファイルの「上からの記載順に」マッチします。
→ よって articles/:idarticles/new より前に記載すると articles#new にたどり着かなくなる。

Error handling by rescue_from

ActiveSupport::Rescuable::ClassMethods
rescue_from
Railsアプリケーションにおけるエラー処理(例外設計)の考え方
Railsアプリの例外構造パターン
Railsでrescue_fromメソッドを使ってエラーハンドリングをする方法
Rack mountable ErrorsController
Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた

他の FW 同様、Rails では各所で raise された例外について上位クラス ( ミドルウェア層 / MVC 層 ) で rescue してエラーハンドリングを行っている。Rails には下位クラスで起きた例外をキャッチしてディスパッチするための rescue_from メソッドがあるため、最上位の ApplicationController でコントローラ以下で発生する例外について拾って、カスタムエラー画面のレンダリング ... みたいなことができる。

またカスタム例外を作成したい場合は StandardError を継承して、同じく最上位の ApplicationController あたりで定義してあげればよろし。Rails には 403 を流す例外がデフォルトで定義されていないので適当に ForbiddenError とか作った方が楽かも。

但しこの実装は、コントローラより上位の Rails の内部的な例外や StandardError や基底 Exception はここで拾うべきではなかったり ( マニュアル参照 ) と拡張性に乏しい。もう少し固くするなら別途 gem を入れるなりミドルウェア系クラスを拡張する必要があるみたい。

# 500 Internal Server Error
#
# raise 時のデフォルト例外 RuntimeError が
# 500 Internal Server Error として rescue され
# デフォルトでは public/500.html がレンダリングされる
#
if ENV.fetch('DB_HOST').empty?
  raise 'Missing required environment variables of DB.'
end

# 400 Bad Request
#
# ActionController::BadRequest 例外を raise すると
# 400 Bad Request エラーとして rescue され
# デフォルトではレンダリングなし ( HTTP ステータスコード返却のみ )
#
if params[:user_id] != current_user.id
  raise ActionController::BadRequest, 'Detected illegal params!'
end

# 404 Not Found
#
# コントローラ内なら ActionController::RoutingError
# モデルロジック内なら ActiveRecord::RecordNotFound
# これらを raise すれば 404 Not Found として rescue され
# デフォルトでは public/404.html がレンダリングされる
#
raise ActionController::RoutingError, 'Missing parameters!'
raise ActiveRecord::RecordNotFound, 'Results not found...'

Cheap error handling

# config/initializers/cheap_error_handling.rb

# Cheap error handling.
# @see https://gist.github.com/yano3nora/3dadc56b5970a23a0528d9d4066b2110#cheap-error-handling

# Custom error classes.
class ForbiddenError < StandardError; end

# Handling exceptions at ErrorsController.
Rails.application.configure do
  config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
end
# controllers/errors_controller.rb

class ErrorsController < ActionController::Base  # Not ApplicationController.
  layout 'error'  # Should logic less template.

  # Rescue exception show method raised.
  rescue_from RuntimeError, with: :handle_500
  rescue_from StandardError, with: :handle_500
  rescue_from ForbiddenError, with: :handle_403
  rescue_from ActiveRecord::RecordNotFound, with: :handle_404
  rescue_from ActionController::RoutingError, with: :handle_404
  rescue_from ActionController::BadRequest, with: :handle_400

  # config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
  def show
    raise request.env["action_dispatch.exception"]
  end

  private def handle_400(e = nil)
    @status_line = '400 Bad Request.'
    @exception   = e
    render template: 'errors/error',
      status: 400,
      layout: 'error',
      content_type: 'text/html'
  end

  private def handle_403(e = nil)
    @status_line = '403 Forbidden.'
    @exception   = e
    render template: 'errors/error',
      status: 403,
      layout: 'error',
      content_type: 'text/html'
  end

  private def handle_404(e = nil)
    @status_line = '404 Not found.'
    @exception   = e
    render template: 'errors/error',
      status: 404,
      layout: 'error',
      content_type: 'text/html'
  end

  private def handle_500(e = nil)
    @status_line = '500 Internal server error.'
    @exception   = e
    render template: 'errors/error',
      status: 500,
      layout: 'error',
      content_type: 'text/html'
  end
end
<!-- views/layouts/error.html.erb -->
<!-- ErrorController は ActionController::Base 継承なのでロジック控えめで -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>
      <%= @status_line %> | <%= t 'words.app_name' %>
    </title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    <script src="/js/bundle.js" defer></script>
  </head>
  <body>
    <header>
      <!-- メニューなしヘッダとかてきとうに -->
    </header>
    <main>
      <%= yield %>
    </main>
    <footer>
      <!-- てきとうに -->
    </footer>
  </body>
</html>
# views/errors/error.html.erb

<article class="container">
  <h2 class="heading">Error <small class="text-muted">/ <%= @status_line %></small></h2>
  <blockquote class="blockquote"><%= @exception.message %></blockquote>
  <div class="m-3"><%= link_to 'Back', :back, class: 'btn btn-light' %></div>
</article>
# 因みに RAILS_ENV=development で本番と同じエラー画面を出すなら
#
# config/environments/development.rb
#
config.consider_all_requests_local = false

SECURITY

Rails セキュリティガイド
Railsのセキュリティ対策で調べた事

XSS

Rails 上の ERB で出力を行う場合、自動的に HTML エンティティ ( HTML entities and special characters ) がエスケープされ XSS 対策となる。ただし、以下メソッドではエスケープをキャンセルするので注意。

<!-- raw -->
<%= raw "<b>hanako</b>" %>

<!-- '==' -->
<%== "<b>hanako</b>" %>

<!-- html_safe -->
<p><%= "<script>alert('a');</script>".html_safe %></p>

SQL Injection

Rails SQL Injection

# 平文のぶち込みは NG name: "' or 1=1 --'" などで死ぬ
User.where("name='#{params[:name]}'")

# 基本プレースホルダーを利用していれば安全
User.where(name: params[:name])
User.where('name = ?', params[:name])              # 同義
User.where('name = :name', {name: params[:name]})  # 同義

protect_form_forgery

CSRF への対応策 - Rails セキュリティガイド
リクエストフォージェリからの保護
protect_from_forgery
ActionController :: RequestForgeryProtection

Rails の CSRF 対策のやつ。コントローラでこいつをコールするとビューに Token 吐いてくれて、フォームの送信時に検証してくれる。Rails はデフォルトで、GET 以外のリクエストを受ける際に X-CSRF-Token リクエストヘッダに ↑ で吐いてる Token がセットされていないと、これを Token Invalid として弾くようになっている。

↑ GET 以外と書いたが、実際は XmlHttpRequest ( 要は Ajax ) のような js 起点のリクエストでは、GET も検証されるぽい。2020 年現在だいたいの web サーバでは、XHR リクエストは同じ Origin からのみ許可される仕様なので、CSRF 保護から除外することも検討して良い。但し、サーバ設定の CORF で * みたいに全許可してたら前述した Origin 保障は当然ないので、必ず CORF 設定を確認しておくこと。

Rails の JavaScript ユーティリティ @rails/ujs ( rails-ujs ) を使うと、このリクエストヘッダに Token を埋め込んでくれる ( 多分ページから要素取得 ) ので意識する必要はそこまでない。またこのライブラリを使って Ajax する際もヘッダを自動でうにょってくれるみたい。これを使わず jQuery など別ライブラリで Ajax する際には、自分でリクエストヘッダをこねる必要があるので注意すること。

また外部サービスの Webhook リクエストを受けたり、Rails が API サーバとして機能するようなシステムでは Token を含んだビューのレンダリングが行われないため with: :null_session で例外スローを抑止したり、exceptonly を使って認証を部分的に外すなどの措置が必要。その際は別途認証を必ず設けること。

ハックな手段だが verify_authenticity_token をオーバライドして独自認証を設ける手段もあるみたい ref

# application_controller.rb
class ApplicationController < ActionController::Base
  # 以下にプラスして form_helper 使っとけばトークン認証が走る
  protect_from_forgery  with: :exception  # with: :exception がデフォルト
  
  # with: :null_session だと検証失敗時にセッションを空にしてくれる
  # 利用ケースとしては、API のリクエストを受けるエンドポイントなど
  protect_from_forgery with: :null_session

  # rails 5 以降は prepend ( 先行実行 ) false デフォルトなので
  # 何らかのフックの後続に書いた場合はかならず prepend: true にしておく
  protect_from_forgery  prepend: true
end

Built-in API

try

Object#try RailsのObject#tryがダメな理由と効果的な代替手段(翻訳)

Ruby 2.3 からの ぼっち演算子 - Safe Navigation Operator である &. に似た挙動をする。nil チェックをすっ飛ばして「メソッドが呼び出せるときだけ呼び出す」という便利なエラー握り潰しメソッド。ロジック層ではガツガツ使うべきでない。

# try
@person.try(:non_existing_method) # nil となり NoMethodError を回避

# try!
@person.try!(:non_existing_method) # メソッドが定義すらされていなければ NoMethodError
<ul>
  <% @user.try(:articles).try(:each) do |article| %>
    <li><%= article.title %></li>
  <% end %>
</ul>

<!-- &. で書くとこうかな ... ? -->
<ul>
  <% @user&.articles&.each do |article| %>
    <li><%= article.title %></li>
  <% end %>
</ul>

empty? blank? present?

Ruby on Railsのオブジェクト存在チェック4種

ruby にはもとから object.nil? メソッドがあるが、Rails にはこれに似た存在確認用メソッドが標準で備わっている。

obj.nil?      # nil なら true
obj.empty?    # nil ではないが中が falsy なら true
obj.blank?    # nil または falsy なら true
obj.present?  # nil でないかつ中身が falsy でないなら true

debug / Rails.logger.debug

<%= debug @user %>
Rails.logger.debug user

# オブジェクトを展開するなら .inspect メソッドを利用
Rails.logger.debug user.inspect

TimeWithZone

Railsタイムゾーンまとめ
RubyとRailsの日付操作周りについてまとめ

Ruby の Date や DateTime は require しないと使えないので Rails 上ではこっち使ったほうが良いかも。

Time.current.class     # ActiveSupport::TimeWithZone

Time.current           # Fri, 18 Aug 2017 18:12:21 JST +09:00
Time.current - 1.days  # Thu, 17 Aug 2017 18:12:26 JST +09:00

Time.new(2017, 9, 1).to_s(:default) # 2017-09-01 00:00:00 +0900
Time.new(2017, 9, 1).to_s(:long)    # September 01, 2017 00:00
Time.new(2017, 9, 1).to_s(:short)   # 01 Sep 00:00
Time.new(2017, 9, 1).to_s(:db)      # 2017-09-01 00:00:00

# 文字列パース ( 以下は同義みたい )
Time.zone.parse('2016-11-11T12:23:42+0000')
'2016-11-11T12:23:42+0000'.in_time_zone

# user.timezone に依存させる
Time.now.in_time_zone(current_user.time_zone)

# config/initializers/time_formats.rb で追加可能
# Time::DATE_FORMATS[:ja] = "%Y年%m月%d日
# Time::DATE_FORMATS[:ymd] = "%Y%m%d"
Time.new(2017, 9 ,1).to_s(:ja)  # 2017年9月1日

# config/locales/ja.yml による追加ならこう
I18n.l(Time.current, format: :long)

ActiveSupport::Integer / ActiveSupport::Duration

ActiveSupport::Durationの期間処理メソッド(1)演算、比較など
Rails の便利なメソッド(数値オブジェクト編)

Rails 内での数値リテラル ( Integer オブジェクト ) は ActiveSupport::Integer を継承しており ActiveSupport::Duration 系の便利な日時変換のメソッドが数値リテラルから直接叩ける。ぎょっとするけど便利。

# 今日が 2019-12-09 だとして、前日の 00:00:00 時点を呼び出す。
1.days.ago.to_time.beginning_of_day    # 2019-12-08 00:00:00 +0900

# 月、年もある。
1.months.ago.to_time.beginning_of_day  # 2019-11-09 00:00:00 +0900
1.years.ago.to_time.beginning_of_day   # 2018-11-09 00:00:00 +0900

# 翌日ならこう。
1.days.since.to_time.beginning_of_day  # 2019-12-10 00:00:00 +0900

# 当然、当日の 00:00:00 時点ならこう。
Time.now.beginning_of_day  # 2019-12-09 00:00:00 +0900

# 23:59:59 ならこう。
Time.now.end_of_day        # 2019-12-09 23:59:59 +0900

# n 日間を秒で返すみたいなこともできる
seconds = 5.days.to_i  # 432000

ActiveSupport::StringInquirer

今更ながらシリーズ(2) StringInquirer

Rails.env == 'production'Rails.env.production? は等価。これは Rails.env が ActiveSupport::StringInquirer という String を継承したクラスに変換されているため。

String#inquiry で String => ActiveSupport::StringInquirer への変換が可能。

s = "hoge".inquiry # => "hoge" 
s.class            # => ActiveSupport::StringInquirer
s.hoge?            # => true 
s.fuga?            # => false  
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment