Railsアプリを扱っていると、↓のような「オブジェクト1のIDを引数にするメソッド」をたまに見かけます。
class BlogEntry < ApplicationRecord
belongs_to :user
# ブログ記事を作成する。user_id は User の id
def self.create_entry(title:, body:, user_id:)
h = { title: title, body: body, user_id: user_id }
# 内容をチェックしたり、値を追加したりするなどの、前処理をする
self.create!(h) # エントリ作成
end
end
しかし、私はこれはアンチパターンであり、idではなくオブジェクトそのものを渡す方が良いと思います。
def self.create_entry(title, body, user_id)
↓
def self.create_entry(title, body, user)
IDを引数にすることの問題点
オブジェクトではなくidを渡しているのは、
- オブジェクトを引数にすると、メソッド呼び出しが遅くなる
- オブジェクトを引数にすると、呼び出し元でオブジェクトを取得する処理(
User.find(params[:user_id])
とか)をしなければならなくなり、SQLを実行する回数が1回増えてしまう
といった考えからだと思いますが、どれも誤りです。
オブジェクトを引数にしてもメソッド呼び出しは遅くなりません。C++やGoでは、構造体を引数にすると値のコピーが発生して速度が落ちることがあります。しかし、Rubyでは引数は参照渡しされるので、オブジェクトを引数にしても、整数(id)を引数にしたときと速度は変わりません2。
idを引数にしても、SQL呼び出し回数は減りません。idを引数に取ようなメソッドでは、
- id に対応するオブジェクトが存在するかのチェック
- オブジェクトが、前提条件を満たしているかのチェック
などもしなければならないはずです(たとえ今は不要でも後から必要になることは少なくありません)。すると、結局 User.find
などを呼び出すことになります。
現実には、後付けで機能追加していった結果、コントローラーとモデルの両方で、オブジェクトの存在確認をしているので、むしろ呼び出し回数が増えているケースすら見たことがあります。
# 後付けで修正した結果、無駄なSQL呼び出しをしているケース
# Controller側のコード
unless User.exists?(id: params[:user_id])
render :error, alert: "ユーザーが見つかりません"
return
end
BlogEntry.create_entry(title: title, body: body, user_id: params[:user_id])
# BlogEntry
def self.create_entry(title:, body:, user_id:)
user = User.find_by(id: user_id)
raise "ユーザーが見つかりません" if user.nil?
raise "ユーザーが凍結中です" if user.frozen?
# 実際の処理
end
IDではなくオブジェクトを引数にするべき
こういうわけで、Railsでメソッドを設けるときは、user_id
とか entry_id
みたいなIDではなく、
user
, entry
のようなオブジェクトを引数にするようにしてください。
上述のように、オブジェクトを引数にしてもC++やGoのように遅くなることはありませんし、.find
を二重に呼び出す必要もなくなるのでかえって速くなるはずです。