Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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

概要

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


  • 2018/04/18: 初版公開
  • 2023/03/16: 更新

参考: Rails API touch -- ActiveRecord::Persistence
参考: Rails API no_touching -- ActiveRecord::NoTouching

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: スコープをモデルの外でチェインするのはやめておけ(翻訳)

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


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。