運用に耐えるRailsによるWebアプリケーションの作り方

筆者は、ここ3年間ずっとRails(3.2, 4.1, 4.2)でのWebアプリケーション開発・運用に携わってきました。開発のスタイルも受託・自社サービス開発の両方を経験してきました。

その経験を踏まえて、長期間の運用に耐えられるかどうかという観点から、RailsでWebアプリケーションを開発する上で気を付けるべき点をまとめてみます。

なお、運用と言う言葉は色々な解釈ができますが、このqiitaでは『サービスを安定して提供しながらも継続的かつ安全に機能拡張を続けること』という定義で使います。 1

1. DB/Model周り

1.1 テーブルのカラム制約とModelの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設計をする方法があります。

例えばPostUserという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_contextshared_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_helperconfig.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.todayTime.now は、システムのタイムゾーンに依存するため、絶対に絶対に絶対に絶対にrailsでは使わないようにしましょう。事故って運用担当者を泣かせます。絶対に Time.zone.todayTime.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で動かしてしまう、という気持ちも分かるのですが、餅は餅屋ということで、適材適所で使う道具を使い分けるということを時には思い出すことも重要です。


  1. 「新機能は実装する」「サービスも守る」「両方」やらなくっちゃあいけないのが(ry 

  2. しかもこういう事案ほど非エンジニアの人への説明がすごく難しいので尚のことつらい 

  3. 因みにhas_xxx, belongs_toの両方に追加してどっちかをdestroyすると、無限再帰呼び出しに陥ってStack too deepになります。初めて見た時はビビります。 

  4. ホントはエラー表現についても色々語りたいことがあるのですが紙面の関係で割愛します