ActiveRecord::Base#touchでレコードの数だけupdateがコールされてしまうのをひとまとめにしてくれる「Activerecord::DelayTouching」

ソフトウェア開発

子レコードにbelongs_to :parent, touch: trueなどと宣言した場合、親レコードにaccepts_nested_attributes_for :childrenと宣言した上で親レコードを更新すると、子レコードの数だけUPDATE文が実行される動きになる。本来1回で済むはずのUPDATEの実行がN-1回分余計に実行されるわけで、これをひとまとめにしてくれるGemがactiverecord-delay_touchingなのです。

内部の動き

DelayTouchingという名前の通り、ActiveRecord::Base#touchをオーバーライドしてActiveRecord::DelayTouching::Stateというクラスの@recordというインスタンス変数にtouch対象のレコードを保存しておいて、

# https://github.com/godaddy/activerecord-delay_touching/blob/master/lib/activerecord/delay_touching.rb#L9
# Override ActiveRecord::Base#touch.
def touch(name = nil)
  if self.class.delay_touching? && !try(:no_touching?)
    DelayTouching.add_record(self, name)
    true
  else
    super
  end
end

レコードの保存時に一気にUPDATEする、という動きをしています。

# https://github.com/godaddy/activerecord-delay_touching/blob/master/lib/activerecord/delay_touching.rb#L96
klass.unscoped.where(klass.primary_key => records).update_all(changes)

この複数レコードを保持している部分はスレッドセーフなのか?という点については、

# https://github.com/godaddy/activerecord-delay_touching/blob/master/lib/activerecord/delay_touching.rb#L40
def self.state
  Thread.current[:delay_touching_state] ||= State.new
end

という形で現在のスレッドにインスタンスを保持するようにしているので、大丈夫そうです。このコードが良い例ですが、クラス変数で情報を保持するというのはグローバル変数を利用しているのに等しいので、注意が必要ですね(参考:Jose Valim,Rubyにおける並行プログラミングのためのいくつかのアイデアを提案。~ RubyKaigi 2013 基調講演 2日目)。

Rails Best PracticesのFetch current user in modelsでThread.currentに情報を保持しているのも、これと同じ理由です。

ただ、いろんなクラスでThread.currentに情報を突っ込んでいると、キーが被った時の動作が怖いなーと思うわけですが。。そのあたり、ご利用は計画的に。

activerecord-delay_touching