3年以上かけて培ったRails開発のコツ集大成(翻訳)
順序は特に決まっていません。
🔗 1. トップレベルにrescue_from
を書く
ルートコントローラにrescue_from
を書くと、その下で発生したすべての例外をキャッチできるので非常に便利です。Webアプリにこれを追加すると、リクエスト/レスポンスのサイクルで実行されるほとんどのコードがさらに便利になります。
シンプルなAPIを例に考えます。rescue_from
を使えば、レコードが見つからない(ActiveRecordがActiveRecord::RecordNotFound
をスローする)場合のアプリの振る舞いを明示的に指定できます。
rescue_from ActiveRecord::RecordNotFound do
api_error(status: 404, errors: 'Resource not found!')
end
🔗 2. コントローラにload_resource
を書く
以前同僚が使っていたのを見て以来採用している別のパターンです。必要なリソースのフェッチをコントローラのメソッド内で行う代わりに、共通のコントローラのフィルタを使い、アクションの実行に応じてフェッチするというものです。
class UsersController
before_action :load_resource
def index
# @usersで何かする
end
def show
# @userで何かする
end
def create
# @userで何かする
end
def update
# @userで何かする
end
def destroy
# @userで何かする
end
private
def load_resource
case params[:action].to_sym
when :index
@users = paginate(apply_filters(User.all, params))
when :create
@user = User.new(create_params)
when :show, :update, :destroy
@user = User.find(params[:id])
end
end
end
これの発展版がdecent_exposureです。私自身はまだ使う機会がありませんが。
ところで、私は主に以下の2つの理由から「よいコードは常にそれ自身が語る」のような言説にあまり賛成できません。
- ある開発者にとってよいコードであっても、別の開発者にとっては悪いコードになることもあります(スタイルは人それぞれなので、それ自体が悪いのではありません)。
- 時間や予算の制約から、手っ取り早く修正してissueをクローズするしかないという状況はいくらでもありえます。最善の(そして最も自明な)ソリューションだと労力も10倍
ということがわかっていてもですになるでしょう。
というわけで、「コードが匂ってるな」と思ったら、恥ずかしがらずにどしどしコメントしましょう😃。
追記(2018/03/28)
2はその後議論になっています。ご利用は計画的に。
強く言いたいんだけど、 rails で `before_action` で `set_instance` みたいにインスタンス変数を暗黙的にセットしたりするの、ほんと嫌いなのでほんとやめてほしい。いや、一つや2つで、特に難しいロジックがないならいいけど、分岐したり3つとか4つとか暗黙的にセットされるとほんと
— HaiTo (@HaiTo_Linux) March 28, 2018
🔗 3. DecoratorやPresenterを使う
しばらく前から、「モデルをファットにして、その分コントローラを薄くせよ」という言説をRailsコミュニティで見かけます。「コントローラを薄くせよ」については同意しますが、ファットモデルについては同意できません😃。
モデルもできるだけ薄くするべきであり、特殊な場合にしか使わないようなプロパティをモデルで自動生成しないことです。そのような場合はラッパークラスを使って(皆さん、これがDecoratorですよ!)必要なメソッドだけを公開しましょう。
PresenterはDecoratorと似ていますが、複数のモデルを扱う点だけが異なります。
🔗 4. モデル配下のワーカーを名前空間化してafter_commit
で呼び出す
User
というモデルがあるとしましょう。あるモデルに関連するバックグラウンドジョブの90%は、User
モデルの作成/更新/削除で発生します(ここでデータが変更されるからです)。ここから、User::CreateWorker
、User::UpdateWorker
、User::DestroyWorker
という3つの汎用ワーカーを導き出せます。利用可能な場合にはこれらのワーカーをActiveRecordコールバックやprevious_changes
と組み合わせて使ってみましょう。ワーカーの呼び出しはafter_commit
で行います。after_commit
を使う理由についてはこちらをご覧ください。
🔗 5. PostgreSQLの配列型は、よほどシンプルでない限り使わないこと
参考: §8.15. 配列 -- PostgreSQL 15ドキュメント
PostgreSQLの配列型はクールですが、私の経験では、時間を節約するより問題を生み出す方が多くなります。PostgreSQLの配列型を使うと(何らかのIDを保存するなど)、後でそのテーブルを目にするたびに必ず私の頭が爆発しました。データベーステーブルのコストは高くありませんが、JOIN
のコストが高いのです。
PostgreSQLの配列型は、よほど小規模な場合にしか使わないことにしています。
- テーブルに保存する要素が少数にとどまり、かつ要素の平均個数が将来増加しないことがわかっている場合(わずかな変動ならありです)
- テーブルがIDや関連付けと一切関わりを持たないことがわかっている場合
🔗 6. Postgres JSONBは優秀
PostgreSQLの配列型と対照的に、PostgreSQLのJSONBは大好きです。スキーマが使えるデータベースは皆に愛されていますし、スキーマレスデータベースを上回る長所があることも知られています。
しかし、スキーマを事前に予測できない場合、スキーマレスデータベースのシンプルさがどうしても必要になることがあります。私は次のような場合にJSONBをよく使います。
- 小さな属性がたくさんあり、しかも親属性で名前空間付けされる可能性もある場合。普通のテーブルでこれをやると、カラムだらけになってしまいます。
- 保存する正確な内容が事前にわからない場合や、プロトタイプ作成を急ぐ場合。
- オブジェクトのハイドレーション(hydration)1を作る場合: オブジェクトをJSON形式でデータベースに保存し、同じJSONからオブジェクトを再構成する。
🔗 7. aasm gemは優秀、ただしステートを変えて初期化しないこと
私はaasm gemが大好きです。ステートマシンでステートや操作を強制可能で、専用のきわめてシンプルなDSLが利用できます。ただし、オブジェクトを初期状態以外のステートで作成するとフックが動作しないという問題が生じます。aasmの内部状態とにらめっこして頑張るか、あきらめてオブジェクトの特定のステートを手動でスキャンすることになります(専用のServiceを作るなど)。
🔗 8 .メールアドレスのバリデーションはgemでやる
メールアドレスのバリデーションに使う正規表現をググると毎回違う正規表現が見つかるのは、もう笑うしかありません。完璧な正規表現を探すのはあきらめて、おとなしくgemを使いましょう。
🔗 9. DecoratorやPresenterを活用して、ビューに渡すインスタンス変数をなるべく1つにする
私にとってRailsの嬉しくない部分です。コントローラからビューにコンテキストを渡すのにインスタンス変数をいくつも使うのは、バッドプラクティスだと思います。Sandi Metzの言うとおり、インスタンス化して渡すオブジェクトは常に1つだけにすべきです。
🔗 10. モデルに保存するインスタンスメソッド名には!
を付ける
モデルのメソッドがオブジェクトを変更してデータベースに保存する場合、メソッド名の末尾に必ず!
を付けて破壊的であることを示しましょう。
こんな簡単なことなのに、開発者(私も含む)は付け忘れがちです。クラスレベルのAPIを厳密に書くことで、コードの品質を高められるようになります。
🔗 11. 単にシンプルな認証機能が欲しいならDevise gemを使わないこと
Deviseはマジックが多すぎます。
🔗 12. Virtusを使って、ActiveRecordでないモデルの属性をより厳密に定義する
私はVirtus gemを多用していましたし、今も使っています。シンプルなPORO(素のRuby: Pure Old Ruby Object)でモデルのように振る舞うオブジェクトを構成でき、属性をある程度厳密に保つこともできます。私は、属性が増えすぎたときに次のようなVirtus向けの独自DSLを書いて属性を操作できるようにすることがよくあります。
# シリアライザなどに定義した属性を再利用できるシンプルなモジュール
module VirtusModel
extend ActiveSupport::Concern
included do
include Virtus.model
if defined?(self::ATTRIBUTES)
self::ATTRIBUTES.each do |group|
group[:attrs].each do |attr|
attribute(attr, group[:type])
end
end
end
end
class_methods do
def all_attributes
self::ATTRIBUTES.map{|i| i[:attrs]}.flatten
end
end
end
# モデルの例
class Model < ActiveModelSerializers::Model
ATTRIBUTES = [
{
attrs: [
:id, :name, :header_text, :is_visible, :filtering_control,
:data_type, :description, :caregory, :calculation
],
type: String
},
{
attrs: [
:display_index, :min_value, :max_value, :value_type,
:number_of_forcast_years
],
type: Integer
},
{
attrs: [:category], type: Array
},
{
attrs: [:is_multi_select],
type: Virtus::Attribute::Boolean
}
].freeze
include VirtusModel
end
さまざまな属性の種類を列挙することも、属性のグループにある種のタグを追加することもできます。おかげで私はニッコニコです😃。
なお、Railsのattributes APIができたので、これで同じか似たようなことができるのではないかと考えています😃。
🔗 13. 外部API参照などの重たい処理にはメモ化(memoization)を使う
もうおわかりですよね😃。
🔗 14. PostgreSQL全文検索はシンプルな用途だけにしておくこと
pg_searchは驚くほど簡単にセットアップできます。tvectorsなどでPostgreSQL全文検索を最適化しなければならない場合は、素直にElasticSearchを使いましょう。PostgreSQLでそれ以上時間をかけるのは無駄です。
🔗 15. 2017年にもなって未だにService Objectとは何かがちゃんと定義されていない
多くの人が同意してくれるService Objectのもっと明確な定義と、どのように実装すべきかを今も探し続けています。
私たちが最近手がけた案件では、あるパターンに従うことで再利用が楽になりました。最初に、モジュールを1つ作成します。これをinclude
すると、perform
という名前のクラスメソッドを作成します。
次に、作成するすべてのサービスで、コンストラクタ(initialize
)をprivate
にします。つまり、コンストラクタのpublicなperform
クラスメソッドだけを呼ぶということです(もちろんRubyのような動的言語ではその気になればprivateメソッドも呼べますが、単に呼びにくくするだけの処置です)。
module PerformerService
def self.included(base)
base.send(:define_singleton_method, :perform) do |url|
begin
return self.send(:new, url).send(:perform)
rescue Exception => e
Rails.logger.error("#{self.class}: Exception raised: #{e}")
end
return nil
end
end
end
class UrlParser
include PerformerService
private
def initialize(url)
@url = url
end
def perform
# ここですごいことをやる
end
end
UrlParser.perform('https://kollegorna.se')
🔗 16. ActiveRecordのエラーメッセージを好みの形に変換する
RailsでAPIを書くと、エラーメッセージはたいていJSON:API形式に従います。つまり、メッセージ(can't be blank
)とメッセージが失敗した属性(user_id
)が出力されます。
この例ではJSONポインタを使っていませんが、これにも同じアイデアを適用できます。
クライアント側では好みに応じて次の2つの方法でこれらを扱います。フォームに移動してuser_id
inputを赤で表示するか、メッセージを連結して「User id can't be blank」などのように読みやすい形に変換するかです。
しかしメッセージに関連する属性がユーザーにとって意味がない場合はどうなるでしょうか。
このアプリで、各ユーザーは新しい投稿(post)を1つ作成できるとします。ただし投稿は1日1回までだとします。モデルで次のようにして一意性を強制します。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
}
}
(はい、DBレベルでも同じようにunique制約をかけるべきですよね、承知しております。しかしここでは仮に、ユーザーが2つの異なるサーバー(しかもそれぞれで同じアプリが動いて同じDBにアクセスする)にアクセスして、運よく(運悪く)2つのリクエストを完全に同時に受け取れないと困るので、このエラーについては扱いません)
このときのメッセージは次のようになります。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "user_id",
"message": "は既に使われています"
}
]
}
ユーザーにこれが表示されても困るだけです。1つの方法は、message
オプションを使うことです。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
},
message: 'さんの投稿は1日1回までです'
}
これで、メッセージは['user_id', 'さんの投稿は1日1回までです']
のように多少読みやすくなりましたが、両方の属性を使う場合はあまり便利ではありません。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "user_id",
"message": "さんの投稿は1日1回までです"
}
]
}
理想は、このメッセージをbase
に移動することです。このメッセージは特定のモデル属性に依存しない、より一般的なカスタム制約だからです。これは、メッセージにカスタムDSLを追加すればできるようになります。
validates :user_id, {
uniqueness: {
scope: :post_id,
conditions: -> { where('created_at >= ?', 1.days.ago) },
},
message: {
replace: "user_id",
with: {
attribute: "base",
message: "ユーザーの投稿は1日1回までです"
}
}
}
def replace_errors(errors)
errors_array = []
errors.messages.each do |attribute, error|
error.each do |e|
if e.is_a?(Hash) && e[:replace]
errors_array << {
attribute: e[:with][:attribute],
message: e[:with][:message]
}
else
array_hash << {attribute: attribute, message: e}
end
end
end
return errors_array
end
これで、使いたい属性に合うエラーが出力されます。
{
"title": "リクエストを処理できませんでした",
"message": "(エラーの詳しい説明)",
"errors": [
{
"attribute": "base",
"message": "ユーザーの投稿は1日1回までです"
}
]
}
🔗 17. 値を返すメソッドでは明示的にreturn
を書く(ワンライナーであっても)
Rubyコミュニティはreturn
文を書かないことにこだわっていると思いますが、私はそこにこだわる理由はない気がしています。実際私は、たとえワンライナーであっても、副作用が目的ではなく、戻り値を目的とすべき場合はreturn
文を追加しています。
Rubyのクールさと表現力を云々することよりも、生産性と(ある種の)安全性の方を優先しましょう。
🔗 18. なるべく丸かっこ()
を使う(ある種のDSLを使う場合を除く)
これも同様です。丸かっこ()
を追加して困ることはありませんし、普段他の言語も使っている同僚も幸せになれます。
🔗 19. env
変数に厳密な論理値型を追加する
私はconfig/sercrets.yml
で次のようなスニペットを使うのが好きです。
<%
booly_env = ->(value) {
return false if value.blank?
return false if (['0', 'f', 'false'].include?(value.to_s.downcase))
return true if (['0', 't', 'true'].include?(value.to_s.downcase))
return true
}
%>
こうすることで、論理値型のenv
変数がtrue
かfalse
のどちらかだけを取るようになるので、コードで使いやすくなります。
development:
enable_http_caching: <%= booly_env[ENV["ENABLE_HTTP_CACHING"] || false] %>
🔗 20. PostgreSQL以外のデータベースをメインで使うなら十分な理由付けが必要
MongoDBはひと頃もてはやされていましたが、やがてMongoDBの欠点が知られるようになりました。
- スキーマレスである
スキーマレスは機能の1つだと思うかもしれませんが、実際には大きな欠点です。データベースにスキーマがあれば、必要に応じてスキーマを少しずつ変更できますし、ツールや保証も得られます。たとえば、SQLにinteger型のカラムが1つあるとすると、これをstring型やtext型に変更することも、デフォルト値の設定やNULL
禁止の設定も可能です。
これはスキーマレスデータベースでは不可能であり、プログラミング言語を用いて高度なレベルで自作する必要があります。スキーマレスデータベースでは、属性の追加や削除も不可能です。基本的に最初のスキーマに縛られてしまうので、一から作り直して正しく移行できることを自力で確認するか、アプリケーションレベルで扱うことになります。
- トランザクションが使えない
- ACIDでない
- クエリが複雑になったときの速度が不十分に思える
メインで使っているデータベースでこんな目に遭っても構いませんか?私はイヤです。個人的にMongoDBの唯一の目玉機能と思えるのは、親ドキュメントに多数のドキュメントを埋め込めることぐらいです。それ以外の機能はおそらくPostgreSQLで事足ります(それにセキュリティアップデートの面倒を見なければならないデータベースシステムが1つで済みます)。
🔗 21. 動的スコープは、他に打つ手がない場合にはよいパターン
Rubyでクロージャ(proc
やlambda
)を定義すると、レキシカルなスコープや環境がクロージャにカプセル化されます。
これは、コードのAという場所でprocを定義したとしても、コードのBという場所でそれを渡して呼び出したときに、procが定義されたAのレキシカルスコープ内で定義されているものであれば、変数でも何でも引き続き参照可能であるということです。言い方を変えると「環境について閉じている」ということです。
これを逆にしたらどうなるでしょうか。たとえばコードのAという場所でprocを1つ定義し、そこでprocを呼んでもまったく意味がないが、コードのBという場所でprocを呼びたい場合にクロージャのレキシカルスコープを変更することで、実行結果にBの環境が反映されるようにするとします。
次の例をご覧ください。
CLOSURE = proc{puts internal_name}
class Foo
def internal_name
'foo'
end
def closure
proc{puts internal_name}
end
def name1
closure.call
end
def name2
CLOSURE.call
end
end
puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> undefined local variable or method `internal_name' for main:Object (NameError)
クロージャの定義時点ではinternal_name
が定義されていないので、当然name2
メソッドは失敗します。
しかし、instance_exec
を使うとprocのバインディング(レキシカルスコープ)を再定義できます。
CLOSURE = proc{puts internal_name}
class Foo
def internal_name
'foo'
end
def closure
proc{puts internal_name}
end
def name1
closure.call
end
def name2
instance_exec(&(CLOSURE))
end
end
puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> foo
成功です。これは、アプリのある部分に書いたコードを、まったく異なるコンテキストで実行できるということです。しかしこれはどんなときに便利なのでしょうか?このあたりをいろいろハックしてみた結果、非常に有用な使いみちの1つはRailsのルーティングでした。
次のようなルーティングがあるとします。
namespace :api do
namespace :v1 do
resources :company_users, only: [:show] do
resources :posts, only: [:index] do
resource :stats, only: [:show]
end
end
end
end
上から以下のルーティングが生成されます。
/api/v1/company_users/:id
/api/v1/company_users/:company_user_id/posts
/api/v1/company_users/:company_user_id/posts/:post_id/stats
:company_user_id
はどうやら不要なので、次のようにしてクライアント側での柔軟性を高めたいと思います。
/api/v1/stats?user_id=:company_user_id&post_id=:post_id
しかしAPIは既に本番で稼働していて変更は困難です。
namespace :api do
namespace :v1 do
resources :company_users, only: [:show] do
resources :posts, only: [:index] do
resource :stats, only: [:show]
end
end
resource :stats, only: [:show], defaults: {company_user_id: proc{params[:company_id]}}
end
end
「え、ルーティングの中にparams
がある?」そのとおり!理由は、次のスニペットを使って、procのコンテキストをコントローラのコンテキストに再バインドしているからです。
def reshape_hash!
self.params = HashWithIndifferentAccess.new(params.to_unsafe_h.reshape(self))
end
これで、このメソッドをbefore_filter
として追加しておけば、このルーティングにuser_id
を送信するとcompany_user_id
として追加されます。
class Api::V1::StatsController < ApplicationController
before_action :authenticate_user!
before_action :reshape_hash!
def index
stats = Stats.new(current_user).all(
user_id: params[:company_user_id], post_id: params[:post_id]
)
render json: stats, serializer: StatsSerializer
end
...
このテクニックをルーティング以外で使ったこともありますが、ほとんどは最後の手段としてです。ご利用は計画的に。
関連記事
- 訳注: hydration(水和物化、水分補給)はシリアライズに似た概念です。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。