2019/09/09加筆: 注意事項
- 多くの人に見ていただいていますが,この記事は2017年12月当時(Railsの最新バージョンが4.2ぐらい)に書かれたものであり,現在は内容がかなり古くなっています
- 2019年9月現在,筆者はRailsどころかwebアプリケーション開発からも離れているため,今の所アップデートする予定はありません(というかできません).
- そのため,本記事を参考にする場合は使用しているRailsのバージョンに合わせて適宜脳内補完しながら読んでいただければ幸いです.
- 本記事に書かれているようなベストプラクティスを検討する上で最善の方法は,Railsの公式リファレンスとRailsのコードそのものを読んで最善策を模索することです.Rails5以上を使っている場合は,こんな古い記事を読まずに,自分で最善の方法について検討することをおすすめします.
筆者は,2014年半ばから2018年3月までRails (3.2, 4.1, 4.2) でのWebアプリケーション開発・運用に携わっていました.開発のスタイルも受託・自社サービス開発の両方を経験してきました.
その経験を踏まえて,長期間の運用に耐えられるようにする観点から,RailsでWebアプリケーションを開発する上で気を付けるべき点をまとめてみます.
なお,運用と言う言葉は色々な解釈ができますが,この記事ではサービスを安定して提供しながらも継続的かつ安全に機能拡張を続けること
という定義で使います. 1
1. DB/Model周り
1.1 テーブルのカラム制約とModelのValidationは絶対に入れる
当然と思われるかもしれませんが,結構validationがちゃんと入ってないproductは多いです.
スタートアップの初期開発フェーズではスピード優先で開発されるため,どうしてもカラム制約やvalidationがタスクから抜け落ちてしまうことがあります.
しかし,カラム制約もなくValidationもないModelは,いとも簡単に異常データをDBに貯め込んでしてしまい,全く関係なさそうなところでエラーを引き起こしてある日突然会社全体を混乱に陥れます.そして,なんの罪もない,運用を引き継いだエンジニアが社内外に謝罪外交に回る日々が待っています. 2
筆者が実際に経験したのはこんな事例です.
ある日,いつものようにproductionにデプロイした直後,app serverが500 responseを大量に返すようになりました.
調べたら,大昔に作られた一部の不正データが原因で,ある時期より前に古いデータを参照しているユーザーにだけエラーが発生する状態になっていました.
デプロイした時点では不正データが入っていたModelにvalidationが入っていたので,完全に油断していました.それ以来,DBのデータ型に依存した処理を入れる時は全環境のDBの対象テーブルのレコードを調べてから実装するようにしています.しかし,最初から正しい制約とvalidationを入れていれば防げた事故でした.
そのため,カラム制約とvalidationは常に追加していきましょう.レビューでも厳しめにチェックすべきです.
筆者は,Modelへのカラム追加を含むPull Requestのレビューでは常に以下の項目を検討し,少しでも疑問があればレビューイに質問するようにしています.
- どうしてこのカラムが必要なのか
- データ型は適切か
- カラム名前は適切か
- どうしてこのカラム制約をつけたのか(or必要そうなのにつけなかったのか)
- どうしてこのvalidationをつけたのか(or必要そうなのにつけなかったのか)
- どうしてこのindexをつけたのか(or必要そうなのにつけなかったのか)
1.2 理由がなければ外部キーを貼る
Railsの create_table
などのmigration用のDSLにはRails4.2.0から外部キーがサポートされるようになりました.理由がなければ外部キーを入れましょう.
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.references :user, foreign_key: true, null: false
t.text :content, null: false
t.timestamps null: false
end
end
end
外部キーによる参照整合制約は,1.1で述べた理由と同様の理由で,不正データが混入することの予防に大いに役立ちます.
外部キー導入については思想の違いから強固に反対されることがあるのですが,少なくとも,正しくテーブル設計をしている限り,外部キーによる参照整合性の担保を導入しても問題にならないというのが筆者の考えです.実際,外部キーを導入したことが原因のトラブルは経験したことがありません.
外部キー導入でよく問題になるレコード削除順の煩わしさも,Railsならdependent: :destroy optionをhas_many
,belongs_to
の定義時に追加することで,親モデルでdestroyを実行するだけで子テーブルのレコードから先に削除することで整合性違反にならないようにレコードを削除できます. 3
しかし,開発チームのポリシーや,その他やんごとなき理由で外部キーを使えないケースもあると思われるので,その辺はまぁよしなにやりましょう.すでに何年も外部キー無しで運用されてきたようなWebアプリケーションだと,後から外部キーを追加するのはデータ整合性の調査や,アプリケーション本体のデグレを防ぐための事前調査などが必要となり,導入コストは高くなる可能性が考えられます.しかし,まだ規模が小さいうちならまだ導入コストは低い傾向にあるため,早めの対応をおすすめします.
1.3 Fat ControllerよりもFat Modelを選ぶ
これも時間がないとやりがちな話ですが,ビジネスロジックにまつわる実装をすべてControllerのactionにベタ書きしているケースがあります.気持ちはわかります.
しかし,多分そのビジネスロジックは,いずれController以外からも使いたくなります.
例えばdelayed_jobやsidekiqで実装した非同期ワーカーと,rakeタスクなどで使いたくなるケースが多いです.場合によっては,viewやMailerからだって使いたくなるかもしれません.実際似筆者も,何度もControllerからビジネスロックを引っぺがしてModelやlib配下に実装したModuleなどに移設するリファクタリングをしてきました.
もちろん,Controller以外でも使う必要が発生したらその時初めてリファクタすればいいというのはその通りです.しかし,そのリファクタリングするコストは,本当に必要な時に払えるコストであるという保証はありません.
仕事でコードを書いている以上,どうしようもなくスケジュールが厳しい状況で機能を実装しなければいけないことは多々あります(大人の事情などで).そんなスケジュールを押し付けてくる組織が悪いという話があるかもしれませんが,体力で乗り切らなければならない時がなんだかんだで多いのがエンジニアというお仕事です.払えるリファクタリングコストは払えるうちに払っておいた方が無難です.
また,dryにすることでコードの見通しがよくなる以外の効果としては,ビジネスロジックのテストを局所化できるという効果もあります.
むしろ,こっちの効果の方が運用においては恩恵が大きいと考えています.
Controllerのactionに複雑度が高いコード,例えば分岐の多いコードなどがゴリゴリ書いてあると,テストケースの増加に伴ってController specが肥大化します.
本来Controller specはinputとなるparamsに対する処理結果だけをテストするものだと筆者は考えていますが,ビジネスロジックのテストもController specに混在すると,前提条件の異なるパターンを大量に用意しなければなりません.
そうなると,あとで見返した時に,どのケースをテストしていて,どのケースをテストしていないのかという網羅性がわかりづらくなることがあります.paramsは同じなのに,actionにベタ書きされたビジネスロジックの条件網羅のために,生成してるテストデータの違いでパターンを分岐するようなことを始めたりすると,相当読みづらいテストが出来上がります.さらに,認証レイヤーに依存するテストケースも混じってくると,もうカオスです.人間の手には負えなくなります.
一方,Modelにビジネスロックを実装するとModelの特定のメソッドに対するテストだけで済み,httpレイヤーは完全に無視できます.oauth認証をパスできるケース,できないケースなんてのをもう気にする必要はありませんし,純粋にビジネスロックだけのテストになるので分かりやすいテストが書けます.
以上の理由から,筆者は実装する場所に迷ったら,Fat ControllerにしていくよりはFat Modelに寄せていく方針を推奨します.それでもメンテが厳しくなったら,そこで初めて,Serviceレイヤーの導入や,DDD寄りの設計に寄せていくなどの別のアプローチを検討するのが良いと思います.
1.4 scopeで検索ロジックをdryにする
全く同じwhere
条件をコピペして使いまわしていたら,もうそれscopeにまとめましょうよ,という話です.これも意外とやってしまいがちですが,scopeにまとめてしまえばdryになるし,human readableな名前をつけることでそのscopeの意図が明確になります.
やりがちなパターンですが,削除フラグ持ちのモデルを取ってくる時に毎回
Post.where(deleted_at: nil)
なんてのをやってたら
class Post < ApplicationRecord
scope :active, -> { where(deleted_at: nil) }
end
という感じでscoeを定義して,
Post.active
のように使うとか.一例ですが(論理削除ならparanoiaとか使った方がいいですね)
1.5 Concernでコード全体の見通しをよくする
Fat Modelになって発生する問題の1つに,コード量が増えて見づらくなる問題があります.Concernである程度の粒度で切り出してしまいましょう.で,model specもConcern単位にファイルを分割しましょう.
サービスクラスを作るというのが最近Rails界隈でも流行ってますが,Modelの見通しが悪いという理由で,サービスクラスを安易に作るのはあまりお勧めしません.サービスクラスほど,チーム開発における統制の取りづらい実装パターンはありません.1人で規律を守って書いているなら良いですが,チーム開発では大抵サービスクラスが乱立してカオスになります.
個人的にオススメする回避パターンは,どのModelがそのロジックを持つべきか?を考えてそのModelのConcernとして実装することです.その方がロジックのコンテキストが明確になるし,どこからビジネスロジックを呼び出せばよいかが明確になります.
筆者がよくやるパターンとしては,Model名の複数形のModuleを作り,その配下に個別のConcernを追加していくnamespace設計をする方法があります.
例えばPost
とUser
という2種類のModelがある場合は以下のようなConcernをModuleとして定義します.
- Posts
- Posts::Export
- Posts::Analytics
- Users
- Users::Auth
- Users::Export
Posts
Users
には何を書くのかという迷いは発生する場面が出てきそうですが,そこはチームでルールを決めながら運用してください.それに,後からメソッドをConcern間で載せ替えるにしても,レシーバーは同じModelクラスのままなので,割とカジュアルにリファクタリングすることができます.
2 Controller周り
2.1 actionは基本的にはCRUDのみを書く
RESTに則ったresource設定をしていれば,大抵はCRUD(index, show, new, create, edit, update, destroy)のみで完結するケースがほとんどだと思います.しかし,例えばbulk処理をするEndpointを用意したくなったり,csv/tsvによるimport/exportを実装するためにCRUD以外のEndpointを実装したくなる時もあります.
RESTを守って実装してくると,そういった要件の実装に頭を悩ませるケースがあります.しかし,素直にそのまま実装してよいかと思います.
無理にcreateやupdateに,特殊なparameterでの指定やformatで処理を分岐するような処理を入れまくると,すぐにactionが数百行のメソッドになり手に負えなくなります.
ただ,基本的にはCRUDで済むものはCRUDに執着させるべきです.
例えば,Restful APIで特定のリソースの状態を問い合わせて,booleanで結果をレスポンスさせるようなEndpointをたまに見かけますが,それ,showじゃダメなんでしょうか.特定の状態に更新するだけのEndpointはupdateに更新したいパラメータ渡すんじゃダメなんでしょうか.
場合によっては,フロントエンドがど~~~~~してもそのIFのEndpointがないと実装できない,っていう時があり,苦肉の策で特殊なユースケース専用のEndpointを実装しなければならないこともありますが,基本的には断るべきです.actionが増えれば増えるほどサーバーサイド側のメンテコストは跳ね上がり,また,似たようなことをするactionが乱立した結果,フロントエンド側にもどのEndpointを使うべきかを考えさせる要因になります.
基本的に,CRUDのEndpointのみが実装されている前提を守っていれば,フロントエンド側から見てもわかりやすいAPIとして提供できますし,バックエンド側の負担も小さくなります.なので,まずはCRUDのみの実装で,解決できないかを検討すべきです.RESTful APIの設計方針について書かれた文書にはよく書いてありますが,そうなるようにresourceをうまく設計することが重要です.
2.2 before_actionの適用対象を限定するときはexceptで対象を除外する
チームで開発していると宗教論争になりやすいやつですが, before_action
で適用するactionを限定するときに only
を使うか except
を使うかという話です.
「どっちでも同じじゃん」って思ったそこのあなた,正しいです.どっちでもやりたいことは実現できます.
しかし,一度実装されてproductionリリースされた後,さらにそのControllerにactionを足す時のことを考えるとどうでしょうか.
例えば,とあるbefore_actionが, 基本的にはすべてのactionで実行してほしい処理
として実装されていたとします.実装されてから数か月後,新しく追加されたactionに対してそのbefore_actionがちゃんと実行される確率が高いのは, only
で対象を指定した時と except
で指定していた時のどちらでしょうか.筆者個人の意見としては except
で指定していた時だと考えます.まぁちゃんとレビューしてりゃどっちでも大丈夫という話ではありますが,それでも人間のすることなので必ずミスはあります.そのミスを防げる確率でいえば, except
で指定していた時だと考えます.
only
は明示的に追加しなければ before_action
の処理対象になりません.したがって,新しいactionを追加したときに,うっかり付け忘れると絶対に実行されません.一方, except
で限定している場合は,何もしなければ実行されます.実行したら問題のある before_action
の可能性もありますが,大抵の場合は,そのactionでエラーになるように before_action
で実行するメソッドが実装されているはずです.
どちらも大きな違いはないように見えるかもしれませんが,実際に筆者は only
で認証処理のメソッドの実行対象を限定していたために,新しく追加したactionでその認証処理を only
に追加するのを忘れてしまい,インシデントになった事例を見たことがあります.
ちゃんとレビューをしてテストを書いてE2Eをやっていれば防げた事例かもしれませんが, only
による指定方式は忘れた場合のセーフティネットが一切効かないため,このような事故を起こす確率が except
を使う場合よりも若干高い,と筆者は考えます.
2.3 エラーはrescue_fromで一か所でハンドリングする
しましょう.htmlのviewを返すwebアプリケーションだとちゃんと守られているケースが多いのですが,Restful APIだと,至る所で適当にrescueして,フォーマットがバラバラなjsonを返している例をよく見かけます.↓
render json: {}, status: 400
render json: { error: 1 }, status: 400
render json: { error: 'invalid' }, status: 400
エラー時のbodyのフォーマットは軽視されがちですが,internalなAPIであっても,ちゃんと方針を決めた方が無難です.そうしないと,フロントエンドチームと
- 「ちょめちょめのAPIで400返ってくるんですけどー」
- 「bodyに何が入ってます?」
- 「
error
っていう文字列だけです」 - 「oh…」
- 「ペケペケのAPIはちゃんとそれっぽいエラーメッセージ返してくれるんですけどねー」
みたいなやり取りを幾度ととなく繰り返すハメになります.
最低限,bodyにサーバー側できめたエラーコードを入れて返すだけ,という方針でもいいので,インターフェースを統一すると解決コストの削減に大いに役に立ちます. 4
さて rescue_from
ですが,色々実装方法は考えられると思うのですが,筆者の場合はApplicationControllerにまとめてrescue_fromを定義し,Controllerによって振る舞いを変える必要な場合は,rescue_fromをoverrideして対応することが多いです.こんな感じ.
class ApplicationController < ActionControler::Base
module CustomError
class BadRequest < StandardError; end
class Unauthorized < StandardErorr; end
class Conflict < StandardError; end
end
rescue_form ActiveRecord::RecordInvalid, with: :render_400
rescue_from ActiveRecord::RecordNotFound, with: :render_404
rescue_from ActionController::RoutingError, with: :render_404
rescue_from CustomError::BadRequest, with: :render_400
rescue_from CustomError::Untuhorized, with: :render_401
rescue_from CustomError::Conflict, with: :render_409
rescue_from StandardError, with: :render_500
def render_400(e)
render_error(e, 400)
end
def render_401(e)
render_error(e, 401)
end
def render_404
render_error(e, 404)
end
def render_409
render_error(e, 409)
end
def render_500(e)
Bugsnag.auto_notify(e)
render_error(e, 500)
end
def render_error(e, status)
render json: e, serializer: ErrorSerializer, status: status
end
end
3. その他雑多な話
3.1 specは可読性と書きやすさのバランスを考えながら書く
チーム開発してると,rspecの書き方に悩むことが多いと思います.人によって派閥が違うため,無用な争いを起こしやすいです(筆者の経験から).
これまで直接または関節的に見聞きしたrspecの宗派には,以下の様なものがありました.(他にもあるかもしれませんが)
- rspecのDSLは全部使いたい.マニアックなmatcherもガンガン使ってくぜ派
- 絶対にDRYにしたいでござる.
shared_context
とshared_example
使いまくる派 - DRYとか無理なので常に全部
it
にべた書きでござる.before? after? let? 知ったこっちゃない派 -
let
は遅延評価で分かりづらいので絶対つかわないでござる.let!
なら使ってもいい派
どの宗派にもpros/consあるので,どれがいいとは一概に言えないのですが,そもそも,specを書く目的は品質の担保です.なので,そこまで筆者はスタイルにはこだわらず,必要なテストケースを網羅できているかをまず第一考えて書いています.とは言え,あとから別の人が読んでメンテすることを踏まえた可読性も重要です.
普段筆者がRails applicationのspecを書く時に気をつけていることについてつらつらと書いてみます.
spec全般
- まずexampleは書かずに
describe
,context
のみを書いてテストケースを洗い出す.それから個々のexampleを実装する. - exampleの中でテストデータの参照が必要なら
let
を使う.そうでなければ不要なlet
は作らないでbefore
で作成する -
describe
,context
,it
にはちゃんとした説明を付ける.英語がつらければ日本語でもおk. - expectのワンライナー形はあまりメリットがないので使わない
- どのmatcherを使うか迷ったら大人しく
eq
で期待する値と直接比較する -
shared_context
はなんだかんだで再利用されないことが多いので基本使わない.テストデータの集約はFactoryGirlに任せる - mockは最小限にする.例えば外部APIにリクエストを送るメソッドだったらhttpレイヤーをモックするだけに留める.
例: Faraday使って外部APIにリクエストする部分のresponseのmock
let(:success_response) do
Faraday::Response.new(
Faraday::Env.new(
:post, {succeeded: true}.to_json,
nil, nil, nil, nil,nil, nil, nil, nil, 201
)
)
end
before :each do
allow_any_instance_of(HogeApiClient)
.to(receive(:create_hoge))
.with(1,'hogetarou','fugatarou')
.and_return(success_response)
end
Controller/Request Spec
- request specを書く余裕がないチームなら
rails_helper
でconfig.render_views = true
を追加し,controller specでviewとresponseの評価も行う- これは知らないうちにviewを壊していないかを確認するための最低限の救済処置です.ホントはrequest specを別途書いた方がいいです
- 最低限
2xxを返すパターン
4xxを返すパターン
はcontextを分けてspecを書く - 認証が必要なCntrollerの場合,認証が通るケース,通らないケースは必ず書く
- 書かないといつかセキュリティ事故を起こします
- before_actionを安易にmockしない
- 面倒でもテストデータは常にproductionで生成されるものと同じデータを作成してテストする.存在しない外部キーを雑にセットしてvalidationをパスさせたりしない
- 1つのテストパターンに対して必ずステータスコードを確認するexampleを書く
- 1つのexampleで複数回requestを行うテストをしない
- 複数のendpointへのリクエストの組み合わせでテストしたいケースはありますが,そういうのはrequest specで書くか別途e2eを書いた方がいいです.途中でコケた時の調査がものすごく大変なのと,何を評価しているのかわからなくなります.
3.2 監視できるものは監視しといて損はない
gemと少しの設定だけで利用できる外部監視サービスが世の中には沢山あります.
しかも無料で利用開始できるサービスが多いので,試さない理由は特に無いと思います.是非入れましょう.
Error監視系
サーバー監視系
筆者はエラー監視系はBugsnagしか使ったことないのですが,以下のスライドで簡単に紹介していますので,よろしければどうぞ.
3.3 個人情報はログ上でマスクする
Railsには,request parameterの指定された項目をマスクする機能があります.例えば,postされてきたパラメーターに password
という文字があったら,maskして password
の値をログに出さないようにすることができます.
この機能を活用しましょう.個人情報が含まれるwebサービスでは,request parameterに平文で個人の名前や住所が含まれることが多々あります.これがそのままログに出力されると,ログそのものが個人情報となってしまい,気軽に閲覧することができなくなってしまいます.そうなると,ログを用いたサービスメトリクスの計測や,エラー監視が気軽にできなくなってしまいます.
ログはサービスを健全に運用する上で非常に重要な手がかりです.常に自由に活用できる状態にしておくことが望ましいです.
この設定は,rails newした直後では
config/initializers/filter_parameter_logging.rb
で設定されており,初期設定で password
のみが指定されています.
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]
コメントに書いてる通りですが, password
以外にもマスクしたい項目を追加することができます.
Rails.application.config.filter_parameters += %i(
password
email
last_name
first_name
address
phone_number
birthday
)
なお,この機能はActionDispatch::Http::ParameterFilterで実現されています.
request parameter以外にも,以下のように任意のhashをマスクすることができますので,Hashからセンシティブな項目だけをマスクするといった用途にも使うことが出来ます.
filter = ActionDispatch::Http::ParameterFilter.new(%i(password email last_name first_name))
user = { id: 1, password: 'kokoropyonpyon', email: 'hideyoshi@nara-suteki.ac.jp', last_name: '豊臣', first_name: '秀吉', note: '奈良県在住' }
filter.filter(user)
# => {:id=>1, :password=>"[FILTERED]", :email=>"[FILTERED]", :last_name=>"[FILTERED]", :first_name=>"[FILTERED]", :note=>"奈良県在住"}
3.4 分析したいlogはjsonフォーマットで統一するとparseしやすい
Loggerクラスので出力されるログのフォーマットはパースしづらいため,例えばfluentdでログをtailしてBigQueryに格納する,というようなことをする時に困難です.
そこで,parseしたいログは,json形式のフォーマットの独自ロガーを作ってしまうのがおすすめです.
application.rb
で以下のようにloggerを追加します.
config.json_logger = Logger.new(Rails.root.join('log', 'json.log'))
config.json_logger.formatter = proc do |s, _, _, message|
{ datetime: Time.zone.now.to_i.to_s, severity: s, message: message }.to_json + "\n"
end
Rails applicationからは以下のように使えます.
Rails.application.config.json_logger.info('test')
実際に出力されるログは下記です.
{"datetime":"1511577163","severity":"INFO","message":"test"}
3.5 backportのためのモンキーパッチにはバージョンチェックを入れる
Railsが使っているgemで,新しいバージョンに入っている特定の機能だけを使いたい時なんかにモンキーパッチを当ててbackportすることがあります.もちろんそのgem自体のバージョンを上げるのが一番いい訳ですが,検証のための時間が取れないなどの理由でバージョンが上げられないことがあります.
そういうモンキーパッチを当てる時は,かならずパッチを当てるgemのバージョンを指定して,特定のバージョン以下の時だけパッチが当たるようにした方が安全です.
前に,Sidekiq3.5を使っているRails applicationに,Sidekiq4.1のとある機能をbackportするために書いたモンキーパッチでは下記のような分岐を入れました.これにより,パッチを当てる先のRails applicationのSidekiqのバージョンが4.1.1以上にアップデートされたらこのパッチは適用されなくなり,意図せず4.1.0のコードで上書きしてしまうことを防ぎます.
if ::Gem::Version.new(::Sidekiq::VERSION) < ::Gem::Version.new('4.1.0')
# 以下つらつらとOpen Classでモンキーパッッチするコード
end
3.6 最初からi18nを使ってテキストをlocaleファイルに切り出しておく
サービス開始から何年か経った後,国際化対応のためにi18nのlocaleファイルに文字リソースを切り出す作業は,とてつもなくつらいです.本当につらい作業です.
そして,いつ国際化対応が必要になるかは正直誰にも分かりません.ある日突然現場に降ってくるかもしれません.そうなると,viewにベタベタに日本語が書かれたテンプレートからテキストをlocaleファイルに切り出しまくる人海戦術で乗り切るしかありません.
初めからlocaleファイルに切り出されていればその言語ぶんだけlocaleファイルを追加するだけで完結します.悪いことは言いません.XX語圏にしかサービスを展開しないと断言できるサービスであっても,後で泣かないために最初からi18nを使うことをオススメします.
ちなみに,筆者は仕事で日本語・英語・中国語(繁体字)対応のサービス開発をしていますが,localeファイルの修正漏れを防ぐためにi18n-tasksというgemを使っています.これはlocaleファイルの中から未使用の翻訳を見つけたり,view側で使われているけどlocaleファイルに実装されていなかったりする翻訳を見つけてくれるありがたいgemです.
これをspecに仕込むことで,CIの度にlocaleファイルのメンテ漏れに気づくことが出来ます.
require 'spec_helper'
require 'i18n/tasks'
describe 'I18n' do
let(:i18n) { I18n::Tasks::BaseTask.new }
it 'does not have missing keys' do
count = i18n.missing_keys.count
fail "There are #{count} missing i18n keys! Run 'i18n-tasks missing' for more details." unless count.zero?
end
it 'does not have unused keys' do
count = i18n.unused_keys.count
fail "There are #{count} unused i18n keys! Run 'i18n-tasks unused' for more details." unless count.zero?
end
end
3.7 Timezoneを常に気にする
これは色んな所で言われている話ですが,TimeZoneに気をつけましょうねという話.
特に,rubyのDate.today
や Time.now
は,システムのタイムゾーンに依存するため,絶対に絶対に絶対に絶対にrailsでは使わないようにしましょう.事故って運用担当者を泣かせます.絶対に Time.zone.today
や Time.current
を使って下さい.railsアプリケーションにおいて,時間は
ActiveSupport::TimeWithZone
で扱うのが原則です.
railsにおけるTimezoneの扱いは,Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠)に詳しくまとまっていますので,こちらを読むのが良いと思います.
なお,システムのタイムゾーンに依存する日付を返すクラスを使っていないかどうかチェックするするCopがrubocopにあるので,絶対に絶対に絶対に絶対に有効にしておいてください.
Timezoneに起因するバグは本当に調査が困難で再現させることも難しいため,本当に運用担当者泣かせの不具合を起こします.はじめから罠を踏まないためにこの原則は守りましょう.
3.8 金額はIntegerか? Float Decimalか?
これどっちがいいんでしょうね.筆者は散々悩んだ挙句,今は金額のカラムは Float Decimalにすることが多いです.
なぜかというと,最初は日本円だけ対応してればよかったのがある日突然ドル対応という(以下ローカライズの話と同じ)
追記: 金額計算の場合,普通のユースケースでは小数点位置が固定なのでDecimalの方が適切では?というコメントをいただき,確かにその通りだな思ったので修正しました.コメントありがとうございました🙇
3.9 時間がかかる処理は積極的に非同期処理に逃がす
以下の処理はSidekiqなどを使った非同期処理で対応するようにすることをおすすめします.
- メール送信
- 外部システムへのリクエスト
処理に時間がかかるので,unicornのprocessを節約するため,という目的もありますが,それ以上に失敗時に自動でretryできるようにすることが大きな目的です.
メール送信は,Sendgridなどの外部サービスを使っていると,それなりに障害に遭遇して,メール送信が全くできなくなる状態に陥ることがあります.そんな場合でも,手軽にメールを再送できるようにしておくことで,障害対応がラクになります.
特に,Sidekiqの場合,Dashboardから死んだjobを手動で再実行できるようので便利です.もちろん,失敗してretryする前提の非同期jobは何も考えずにretryしてもちゃんと動くように実装しておく必要があります.
3.10 餅は餅屋
Railsは便利なので,何でもかんでもRailsで書こうとしてしまいがちですが,それホントにRailsで書いていいかどうかは,たまに立ち止まって考えた方がよいです.
特にETL系バッチなんかは,rakeタスクを自前で書いてcronで動かすよりは,スケジューリング機能もリトライ機能も備えたembulkなどを使った方が早かったりします.外部ツールのconfigファイルのsyntaxを覚えてメンテするのが面倒でつい使い慣れたrake taskでゴリゴリ書いてcronで動かしてしまう,という気持ちも分かるのですが,餅は餅屋ということで,適材適所で使う道具を使い分けるということを時には思い出すことも重要です.