はじめに
最近RackサーバーをPumaに切り替えたこともあり、スレッドセーフのチェックをがんばっている日々。特定の条件で動的にcallbackをスキップしている箇所があるが、set_callback / skip_callbackはスレッドセーフでないので使えない。じゃあどうしたら良いのよ!?というのがきっかけで、skip_callbackと同等な動作をスレッドセーフでやる方法を調べてみた。
素朴な疑問(:callbacks => falseオプションが実装されていない経緯)
ActiveRecordの作成・更新系のメソッドにはvalidationをスキップするオプションがある(:validate => false)。だったらそもそも、callbackをスキップするオプションだって実装されていても良いのでは?という素朴な疑問が浮かんだ。調べてみると、railsのPull Requestで同じことを考えている人がいた。
https://github.com/rails/rails/pull/4885
結論としては、callbackをスキップするオプションが欲しいと考えてる人は多いようだが、結局railsには実装されず。理由は、Modelの外の条件でcallbackの有効/無効が切り替わるのはユースケースとして間違っている、そもそもcallbackを使うべきではない、ということ。じゃあ、update_columnsみたいなメソッドはどうなんだとか、:validate => falseのオプションはどうなんだとかツッコミが入っているが、、
skip_callbackのスレッドセーフな代替案
attr_accessorを使う
attr_accessorを使って、callbackの有効/無効の状態を保持しておく。たぶんこれが一番きれいな方法。
# Model
attr_accessor :skip_my_callback
after_save{ my_callback unless skip_my_callback }
#controller
Model.create skip_my_callback: true
Observerを使う
モデルの内部的なロジックに関係しないトリガーメソッドは、callbackではなくobserverを使うのがもともとのマナー(らしい)。
configで指定できるので、RAILS_ENVで有効/無効を切り替えたいときはこれが最適。
config.active_record.observers = [:user_observer]
callbackのない別モデルを作る
callbackのない別モデルを作り、そちらのモデルを使ってモデルの更新などを行う。
# Model
class PureUser < ActiveRecord::Base
self.table_name = 'users'
end
# Controller
PureUser.new( user_attributes )
update_columns
DBを直接更新しにいくことで高速化を図ることが目的のメソッドだが、callbackをスキップするためにも使える。ただ、validationもスキップしてしまうし、after_save/after_updateあたりの一部のコールバックだけにしか使えない。
同じようなメソッド:
update_column
update_columns
update_all
update_counters
gemを使う
サードパーティのgemを使う。ざっと調べて、以下のようなgemを見つけた。使うgemが本当にスレッドセーフか確認する必要があるが、、
https://github.com/einzige/sneaky-save
https://github.com/dball/skip_activerecord_callbacks
結論
RAILS_ENVでcallbackの有効/無効を切り替えたいときはObserverを使い、それ以外の場合はattr_accessorを使う。