Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all articles
Browse latest Browse all 1759

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

$
0
0

概要

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

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は使うな、絶対(翻訳)


Viewing all articles
Browse latest Browse all 1759

Trending Articles