Rails: Active Recordのtouchをno_touching
で一時的に無効にする(翻訳)
Active Recordモデルでのtouch
は多くのRailsアプリで広く使われています。特にキャッシュの無効化で便利です。このメソッドは、デフォルトでは現在時刻でupdated_at
タイムスタンプを更新します。以下はモデルでtouch
を使う典型例です。
# app/models/photo.rb
class Photo < ApplicationRecord
belongs_to :user, touch: true
end
新しい写真が作成されたり既存の写真が更新/削除されるたびに、関連付けられているユーザーのupdated_at
属性が現在時刻で更新されます。多くの場合これは期待どおりの動作です(これはActive Recordの珍しいコールバックですが、それほど悪いものではありません)が、何らかの理由でtouch
したくないこともあるでしょう。何かいい組み込みメソッドはないものでしょうか?
問題の分析
touch
を一時的に無効にするのは、パフォーマンス上の理由(大量のレコードを更新するとき)や、after_touch
とかafter_commit
の重複実行を防止または回避するうえで便利です。
しかし後者には設計上の問題が潜んでいる可能性があります。理由は、レコードの内部状態を上書きするような副作用を引き起こす重要なロジックをActive Recordのコールバックに配置すると、たちまち地獄行きになるからです(特にメール通知をトリガーする場合)。しかし現実には多くのRailsアプリでコールバックがこのような形で使われてしまっています。
解決方法
ありがたいことに、ブロック内で一時的にtouch
を無効にするActiveRecord.no_touching
を利用すれば、大幅なリファクタリングや書き直しは不要です。
あるユーザーと、そのユーザーに属するすべての写真を更新する必要があるとしましょう。そしてすべての写真が更新されてからtouch
する必要があるとしましょう。以下のようにできます。
user = User.find(user_id)
ActiveRecord::Base.transaction do
User.no_touching do
user.photos.find_each do |photo|
# userは`touch`されない
photo.update!(some_attributes)
end
end
user.touch
end
何らかの理由で全モデルのtouch
を無効にしたいのであれば、このメソッドをActiveRecord::Base
で呼ぶだけで済みます。
user = User.find(user_id)
ActiveRecord::Base.transaction do
ActiveRecord::Base.no_touching do
user.photos.find_each do |photo|
# どのモデルも`touch`されなくなる
photo.update!(some_attributes)
end
end
user.touch
end
できあがりです!
まとめ
ActiveRecord.no_touching
は、トリッキーな問題が潜む可能性のある問題をまさにお手軽に解決してくれます。しかしこれはアプリの設計に潜む問題に対するダーティハックでもあり、その問題は遅かれ早かれ正すべきです。
訳注
ActiveRecord.no_touching
のソースはたった1行でした。
# File activerecord/lib/active_record/no_touching.rb, line 22
def no_touching(&block)
NoTouching.apply_to(self, &block)
end
apply_to
は以下でした。
# https://github.com/rails/rails/blob/7c70791470fc517deb7c640bead9f1b47efb5539/activerecord/lib/active_record/no_touching.rb#L28
class << self
def apply_to(klass) # :nodoc:
klasses.push(klass)
yield
ensure
klasses.pop
end
...
概要
原著者の許諾を得て翻訳・公開いたします。
参考: Rails API
touch
--ActiveRecord::Persistence
参考: Rails API
no_touching
--ActiveRecord::NoTouching