パーフェクトRailsまとめ
職場である程度Railsでの環境に慣れてきたところで、今のプロジェクトと照らし合せて
パーフェクトRailsで知識を補填することにしました。
わかっている箇所についてはどんどん飛ばしていくので、まとめとしては歯抜けになってしまうかもしれませんが、備忘録として作成しました。
概要
▼Rakeタスク書き方
定型コマンドをまとめておくためのタスク。
desc 'Hello, display Rake Task'
task :hello do
puts 'Hello, Rake!'
end
これでrake hello
で実行できるようになる。
▼Gemfile.lock
今使っているgemのバージョンが記録される。
▼設計思想
- 設定より規約(Convention to Configuration)
- DRY
- REST
- 自動テスト
▼HTTPメソッドについて
- HTML5ではPUT/DELETEを採用してないので、Railsでは
overloaded POST
で行う
▼ActiveRecordについて
- 実際にそのSQLの実行結果が必要になるまでは、DBに対するアクセスは実行されない
▼scopeの設定
class Book < ActiveRecord::Base
scope :costly, -> { where("price > ?", 3000) }
scope :written_about, -> (theme) { where ("name like ?", "%#{theme}%") }
end
- default scope デフォルトで降順に設定、
class Book < ActiveRecord::Base
default_scope -> { order("published_on desc") }
end
▼errorについて
ActiveRecordのオブジェクトにerrorが発生すると、エラーメッセージを参照することができる。
book.errors.full_messages
保存をせずにバリデーションだけを行いたいときはbook.valid?
などとするとバリデーションを通過しているかもわかるし、失敗時にはerrorメッセージが入る
▼メソッドについて
-
!
付きメソッドはバリデーション失敗時例外を発生するActiveRecord::RecordInvalid
-
!
なし 例外を起こさない
例外をrescueして処理を記述したいときもあると思うので、場合によって使い分けるように。
コールバックについて
- 前処理、後処理を綺麗に宣言
- 必ず行いたい処理の実行漏れを防ぐことができる
バリデーション処理については一般的にこういった書き方をする
class Book < ActiveRecord::Base
before_validation do |book|
book.name = book.name.gsub(/Cat/) do |matched|
"lovely #{matched}"
end
end
削除後にログを取るようにする
class Book < ActiveRecord::Base
after_destroy do |book|
Rails.logger.info "Book is deleted: #{book.attributes.inspect}"
end
end
バリデーションによってアクションごとに適用されるされないがあるので、注意が必要。
また、コールバックがまったく実行されないメソッドがあるので、これも注意!
enum型
- 文字列を定義した順番で0から始まる 明示的に
class Book < ActiveRecord::Base
enum status: %w(reservation now_on_sale end_of_print)
# enum status: { reservation: 0, now_on_sale: 1, end_of_print: 2 }
end
ルーティングとコントローラ
- 古典的な例
Rails.application.routes.draw do
get '/books/;id' => "books#show"
#...
end
- 一般的なshowアクションの記述例
class BooksController < ApplicationController
def show
@book = Book.find(params[:id])
respond_to do |format|
format.html
format.json
format.csv
end
end
end
ApplicationControllerはすべてのControllerで使用するヘルパーや属性、挙動を定義する場所です。
コントローラの役割まとめ: リクエストからコントローラの中でアクションを決定し、アクションの中でモデルを通じて必要な操作を定義する。そして、ビューにオブジェクトを渡す。
- コールバックはスキップもできる
skip_before_action
skip_after_action
-
skip_around_action
等のメソッドが既に用意されている。
class ApplicatiolnController < ActionController::Base
before_action :require_login
def require_login
if !@crew.logged_in?
redirect_to login_page_path
end
end
class LoginController < ApplicationController
# ログインするための画面にたどり着けないので
skip_before_action :require_login, only:[:new, :create]
end
▼resourcesルーティングの拡張
拡張は基本的にresources
ブロックの中に記述する
Rails.application.routes.draw do
resources :doctor do
resources :patients
member do
get 'detail'
end
collection do
get 'search'
end
end
end
ブロックに入れることで、リソースの親子関係をルーティングで表現できます。
実は単数形のresource
もある。そのときはindex
へのリソースは作られない
member
やcollection
は基本アクション以外のルーティングを設定することもできる
-
member
リソースに:id
を含める必要があるとき collection
それ以外の場合StrongParametersはMassAssignmentからの脆弱性を除外する
コントローラ名とアクション名が同じなら
render
は省略可
▼variantsによるテンプレートの切り替え
ApplicationControllerにvariantを使うことで、アクセスする端末によって表示するテンプレートを変えることができる
▼jbuilder
基本的にrails new
したプロジェクトなら勝手にgem 'jbuilder'
が入っている。
これを使用している場合、app/views/books/show.jbuilder.json
ファイルを作る必要がある。
json.extract! @book, :id, :name, :price, :created_at
このようにファイルを作成し、アクセスするだけでjsonデータが作成される。
詳細 ( https://github.com/rails/jbuilder )
Sass
SCSS記法とSass記法二つある。
SCSSはCSSと互換性あり。Sassはスタイルをインデントで表現。
拡張子もそれぞれ.scss
と.sass
で2種類ある。
現在主流で使用されているのはSCSSである。
ただ元々Sass記法からSCSS記法が生まれたので、SCSSのこともSassと呼ぶことが多い
-
&
は親セレクタの参照
#main p {
color: #00ff00;
width: 97%;
.redbox {
background-color: #ff0000;
color: #000000;
}
}
a {
&:hover { text-decoration: underline; }
}
@extend
.error {
border: 1px #f00;
background-color: #fdd;
}
.seriousError {
@extend .error;
border-width: 3px;
}
@mixin
, @include
mixinはextendと違い、特定のセレクタに依存しない再利用方法
- mixinで定義
- includeで利用
要するにmixinとincludeはセット
@mixin clearfix {
&:before, &:after {
content: " ";
display: table;
}
&:after {
clear: both;
}
}
img.thumbnail {
float: left;
@include clearfix;
}
その他、@if
@else
@for
@each
@while
なども使え、通常のプログラミング言語のように
演算子をしようして条件分岐、ループなどもできる。
Turbolinks
▼Pjaxとは
ページ遷移の高速化手法。HTML5のpushStateという機能とAjaxを組み合わせた手法。
これによってURLを更新しつつ、ページ全体を描画せずに必要な部分のみを更新できます。
同一のヘッダーを再利用できるために、Javascriptの読み込みなどを簡略化できます。
このPjaxの簡易的な利用方法を提供するのがTurbolinksです。
Turbolinksの読み込みはapplication.jsで行われる
//= require turbolinks
任意のリンクでTurbolinksを無効にする際は、data-no-turbolinks属性を付ける。
▼jQuery-Turbolinks
//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs
//= require turbolinks
読み込み順序は必ず//= require jquery.turbolinks
が先
Railsのロードパス
▼libディレクトリ
役割
- モジュールを配置
- 独自のRakeタスクファイルを配置
libディレクトリには元々requireパスが設定されている。
▼新レイヤーの配置とオートローディング
ファイル名はスネークケース、クラス名はキャメルケースで作ると基本的にオートローディング
新しいモジュール置き場を作りたい、そういった時は?
- appディレクトリ以下に、新しくサブディレクトリを作成
- そのディレクトリをautoloadingに加える
これだけです。
class Application < Rails::Application
# config.autoload_paths += %W(#{config.root}/app/foobar)
config.autoload_paths << Rails.root.join('lib')
config.autoload_paths << Rails.root.join('app/validators')
#...
end
▼pry-rails
このgemにより、rails consoleの中でpryを実行できるようになる。
◎ステップ実行
binding.pry
でブレークポイントを設定することによって、それまでに実行されたオブジェクトを参照、操作することができる。
追加でgem 'pry-byebug'
をinstallすることによってステップ実行を行うことができるようになります。
quit
で終了すると、実行されていた処理が再開されます。
next
step
などのメソッドも追加される。
▼Better-errors
エラー画面を見やすくするもの。
デフォルトの真っ赤な画面にならない。
リクエストが見やすく表示されるので、オブジェクトの中身も見やすくなっている。
▼Spring
Rails4.1から導入。
ロード時間の軽減のためのgem。
Windowsでは無効(まじですか...w)
初回はSpringサーバが起動してからなので、時間はかかりますが、2回目以降からはRakeタスクの実行時間が大幅に短縮されます。
gem spring-commands-rspec
installで、Rspecも高速化できる。
▼i18n Configuration
適用させたい場合、ディレクトリはconfig/application.rb
に追加。
config.i18n.default_locale = :ja
表示させるメッセージは以下のようにymlファイルを使用して設定する。
ymlファイルはconfig/locales/hoge_ja.yml
ja:
activerecord:
models:
event: イベント
attributes:
event: 名前
place: 場所
content: 内容
テストについて
▼なぜテストを書くのかについて
- アプリケーションコードについて考える機会が増える
- 仕様の抜け漏れを防ぐことができる
- 手作業テストをする必要がなくなる
- アプリケーションコードに対して自信を持てる
- リファクタリングを行う際に安心感が持てる
▼モデルのテスト
テストすべき項目
- メソッドが期待通りの値を返すか
- メソッドが期待通りの副作用をもたらしているか
- バリデーションが正しく動作しているか
rails g rspec:model crew
これで空のrspecファイルが作成される。
▼コントローラのテスト
テストすべき項目
- ビューに渡るインスタンス変数が期待通りのオブジェクトを持っているか
- アクションが期待通りのステータスコードを返しているか
- アクションが期待通りのテンプレートを選択しているか
▼エンドツーエンドのテスト
クライアント側から操作して、サーバをブラックボックスとして入出力をテストすること
▼Javascriptのテスト
実際にブラウザが動作するかどうかはseleniumでテストすることができる。
しかし、Railsでの開発では積極的には行われていません。
capybaraでjavascriptをテストする際は、poltergeistというgemを使用する。
▼CI(継続的インテグレーション)
テストコードが巨大になってきた時に、手助けしてくれるツールが存在する。
- Jenkins
- CircleCI
- TravisCI
▼カバレッジ, 静的解析
カバレッジ:テストの網羅率
静的解析:プログラムを実行せずに、プログラムの問題点やバグを調べて分析すること
カバレッジにはC0,C1,C2などのレベルがある。高いほど、網羅性が高く、品質のいいコードである可能性が高い。
DevOps
Dev:アプリケーション開発者
Ops:運用、インフラ担当エンジニア
おたがいの仕事を理解しようという考え。
なぜ仮想環境を作るか?
- ローカルに開発環境を完成させることの意味
- 本番になるべく近い環境を得ること
- クリーンな動作環境を作ること(不要なソフトウェアとの干渉を避ける)
Vagrant
VirtualBoxを利用してVMをたちあげて、セットアップなどをコマンド一つでやってくれる
より実践的なモデルの使い方
Railsの原則:Controllerを薄く、Modelを厚く
しかし、今度はModelが大きくなりすぎる
RailsではRDBの利用を想定している。
オブジェクトとRDBのデータをマッピングするためにいくつもの手法があるが、RailsではActiveRecordで実現している。
言い換えると、Railsでのモデル層で利用するライブラリの名前がActiveRecordである。
ただし、複雑なロジックを、ドメインモデルをRubyのクラスに対応させなければいけないので、ある程度の限界はある。
何も考えずにやっていると、ActiveRecordモデルが肥大化しすぎてしまう。
まず、バリデーションとコールバックを分離することから考えてみましょう。
コールバックをクラスに分離する
シンボルで記述するのが最も一般的かつ推奨(一応ブロックも、オブジェクトも渡すことができる)
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
この方法は非常にシンプルでわかりやすいですが、コールバックの数が増えるに連れてActiveRecordサブクラスが肥大化してきて、責務がどんどん増えていくことになります。
▼解決方法
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new("credit_card_number")
end
class EncryptionWrapper
def before_save(record)
record.send("#{@attribute}=", encrypt(record.send("#{`attribute}")))
end
end
こういった形で分離することができます。モデル層が肥大化すればするほど、この方法を取るメリットは大きくなっていくでしょう。
バリデータをクラスに分離する
コールバックと同様に、こちらもクラスに分離することができる。
ActiveModel::EachValiatorを継承したクラスを定義する
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add attribute, (options[:message] || "is not an email") unless
value =~ /¥A([^@¥s]+)@((?:[-a-z0-9]+¥.)+[a-z]{2,})¥z/
end
end
class Person
include ActiveModel::Validations
attr_accessor :name, :email
validates :email, presence: true, email: true
end
RDBに直接対応しないクラスを作る
ActiveRecordは優れたORマッパーだが、モデル層がRDBの構造と直接依存したものになってしまう。
そういったときは、ActiveModelモジュールを利用することにより、ActiveRecordとほぼ同様の機能を実装することが出来ます。
- バリデーション、コールバック
- 属性名をもとにした動的メソッドの定義
- 属性値に対する変更の保持、確認
▼ActiveModelが提供するモジュール
- ActiveModel::AttributeMethods(メソッドの宣言)
- ActiveModel::Callbacks
- ActiveModel::Dirty(属性値の変化、追跡)
- ActiveModel::Naming(Rails命名規約の導入)
- ActiveModel::Serialization
- ActiveModel::Vailidations
エンティティ, 値オブジェクト
アプリケーションがオブジェクトの同一性をどう捉えるか?
例をあげると、Userクラスは、仮にEmailアドレスが変更されても変更前のUserと変更後のUserは同一であるべきです。
エンティティ:システムでオブジェクトの同一性が重要な意味をもつもの
値オブジェクト:値が同じであればアプリケーション上では、同一であると見なしてよいオブジェクト
エンティティは基本的に同一性を認識する為の識別情報を持っており、Railsの場合は「id」カラムです。ほかの情報が異なっていても、idが同じであれば同一のオブジェクトです。
ActiveRecordのオブジェクトは基本的にエンティティオブジェクトを作成することになる。
▼値オブジェクトによる実装の例
Userクラス
# == schema information
#
# Table name: users
#
# id :integer
# name :string(255)
# prefecture :string(255)
# city :string(255)
# house_number :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
def address
@address ||= Address.new(prefecture, city, house_number)
end
def address=(address)
self.prefecture = address.prefecture
self.city = address.city
self.house_nunmber = address.house_number
@address = address
end
end
Addressクラス
class Address
attr_accessor :prefecture, :city, :house_number
def initialize(prefecture = nil, city = nil, house_number = nil)
@prefecture = prefecture
@city = city
@house_number = house_number
end
def hash
prefecture.hash + sity.hash + house_number.hash
end
def ==(other)
return false unless other.is_a?(Address)
same_prefecture?(other) && same_city?(other) && same_house_number?(other)
end
def same_prefecture?(other)
prefecture == other.prefecture
end
def same_city?(other)
city == other.city
end
def same_house_number?(other)
house_number == other.house_number
end
end
値オブジェクトは、責任を適切に分割することを可能にします。
もし住所を持つべきクラスが増えても、このクラスを再利用すればよいだけなので非常に便利です。
▼composed_ofでActiveRecordを値オブジェクトにマッピング
先ほどのAddressオブジェクトをcomposed_ofでマッピングしてみましょう。
# == schema information
#
# Table name: users
#
# id :integer
# name :string(255)
# prefecture :string(255)
# city :string(255)
# house_number :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
composed_of :address, mapping: [%w(prefecture prefecture), %w(city city), %w(house_number house_number)]
end
Userクラスのprefecture属性に値オブジェクトであるAddressクラスのprefectureを接続しています。
Concern
Rails4からapp/models/concerns
, app/controllers/concerns
ディレクトリが追加された。
デフォルトでは空である。RailsはここにConcernでモジュールを置くことを想定しています。
- アプリケーション全体で利用されるロギング機能
- 変更に対する証跡の記録
- タグによる分類機能
- データの論理削除や有効化、無効化処理
記述を補助してくれるActiveSupport::Concernモジュールが存在する。
サービスクラス
イメージ的にはコントローラとモデルの中間をイメージするのがよい。
一連の処理をサービスクラスに抽出することで、モデルが過剰に複雑になるのを防ぐ。
ただしサービスクラスなどを多用してしまうと、本来ビジネスロジックがモデルに集約されているのが自然なのに、それがサービスクラスに漏れ出てしまうことがある。多用注意。
Railsを拡張する
- RackMiddlewareを作る
- Railtieの仕組みを利用する
- その他、コマンドラインツールを作る
Rackとは、たくさんのアプリケーションサーバとRubyフレームワークをつなぐ規約のこと
PythonのPSGI規約をもとに提案されたのがRack。
WEBサーバ実装と、Rubyフレームワークの中間に存在して、お互いの差を吸収するアダプター的存在。
Rubyの標準的なオブジェクトのみで構成されているシンプルなもの。
Rackに準拠したたった二つの規約に準拠している
1. 1つのHashオブジェクトを引数に取るcall
というメソッドが実装されていること
2. call
メソッドは「ステータスコード」、「ヘッダーを表現したHash」、「eachに反応するオブジェクト」の3つの要素を持った配列を返すこと
慣習的にcallメソッドの仮引数の名前はenvが用いられる。
Railtieとは
Rails公式のプラグイン機構。
Railsの基本的なコンポーネントもRailtieを経由して読み込まれるようになっている。
これにより、以下の処理を簡潔に実現できます。
- 初期化処理を実行
- ジェネレータやRakeタスクをRailsに実行する
- Railsの設定項目を追加し、environment毎にカスタマイズ可能にする
- ActiveSupport::Notificationのための固有のサブスクライバを設定する
Railtie自体もrailltiesというgemで配布されている。
まとめ
実際に現プロジェクトで使用しているTipsも多く含まれており、実際の開発環境に役立てられそうだ。
特にルーティングの細かい指定方法、modelのenum型定義の便利さ、jbuilderの原理や定義方法、コールバックとバリデータのクラス分離、Addressクラスの値オブジェクトによる再利用など、普段普通に使用しているものについて頭の中で整理することができた点が良かった。