概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Rails Quick Tips: Temporarily Disabling Touching with ActiveRecord.no_touching - Karol Galanciak - Ruby on Rails and Ember.js consultant
- 原文公開日: 2018/02/25
- 著者: Karol Galanciak
- サイト: BookingSync
ActiveRecordのtouchをno_touching
で一時的に無効にする(翻訳)
ActiveRecordモデルでのtouch
は多くのRailsアプリで広く使われています。特にキャッシュの無効化で便利です。これはデフォルトでは現在時刻でupdated_at
タイムスタンプを更新します。以下はモデルでtouch
を使う典型例です。
# app/models/photo.rb
class Photo < ApplicationRecord
belongs_to :user, touch: true
end
新しい写真が作成されたり既存の写真が更新/削除されるたびに、関連付けられているユーザーのupdated_at
属性が現在時刻で更新されます。多くの場合これは期待どおりの動作です(これはActiveRecordの珍しいコールバックですが、別に悪いものではありません)が、何らかの理由でtouch
したくないこともあるでしょう。何かいい手はないものでしょうか?
問題の分析
touch
を一時的に無効にするのは、パフォーマンス上の理由(大量のレコードを更新するとき)や、after_touch
とかafter_commit
が何度も実行されないようにするうえで便利です。しかし後者には設計上の問題が潜んでいる可能性があります。というのも、レコードの内部状態を上書きする副作用を引き起こす重要なロジックをActiveRecordのコールバックに配置すると、(特にメール通知をトリガするときに)簡単に地獄を見ることができます。しかし現実には多くの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/375a4143cf5caeb6159b338be824903edfd62836/activerecord/lib/active_record/no_touching.rb#L28
class << self
def apply_to(klass) #:nodoc:
klasses.push(klass)
yield
ensure
klasses.pop
end
...