ActiveRecordのtouchを`no_touching`で一時的に無効にする(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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
...

関連記事

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ