🤕

Dirtyが原因で通知が動かない?Railsの変更検知トラブルを解決!

2024/12/09に公開

こんにちは!
ラブグラフエンジニアのひろです。

今回は、以前紹介した ActiveRecord::AttributeMethods::Dirty を使っていく中で遭遇した問題について書いていきます。
https://zenn.dev/lovegraph/articles/a1c686ccb68403

はじめに

Rails では、モデルの属性が変更されたかどうかを簡単に検知できる便利な仕組みとして ActiveRecord::AttributeMethods::Dirty が提供されています。
この機能を活用することで、変更前後の値を取得したり、条件に応じて特定の処理を実行したりすることが可能です。

しかし、この機能を利用する際には、特にコールバックとの組み合わせで意図しない挙動を引き起こす可能性があります。
本記事では、 Dirty の基本的な機能をおさらいしながら、その罠について解説し、安全な実装方法を提案します。

ActiveRecord::AttributeMethods::Dirty とは?

ActiveRecord::AttributeMethods::Dirty は、Active Record モデルの属性が変更されたかどうかを追跡する仕組みを提供するモジュールです。
この機能を活用することで、例えば次のような操作が可能になります。

  • モデルの属性が保存される前後での値を確認
  • 特定の属性が変更された場合にのみ処理を実行

主なメソッド

以下は Dirty の主なメソッドです:

  • <attribute>_changed_in_place?:インスタンス内で属性が変更されたかをチェック
  • will_save_change_to_<attribute>?:指定された属性が保存時に変更されるかをチェック
  • saved_changes:保存時に変更された属性とその変更内容を取得
  • <attribute>_before_last_save:保存前の属性値を取得

Dirty を使った実装で起こりうる罠

ActiveRecord::AttributeMethods::Dirty は、モデルの属性変更を検知するための便利な仕組みですが、特にコールバックと併用する際には注意が必要です。
Rails のコールバックは非常に強力なツールですが、内部でのモデルの更新操作が、 Dirty の変更検知ロジックに予期せぬ影響を与える場合があります。

たとえば、以下のような状況で問題が発生することがあります。

発生する問題

次のような Album モデルを例に考えてみましょう。このモデルでは、以下の2つの処理をおこなっています

  1. アルバムが公開された場合( visibletrue に更新された場合)、一枚目の写真をサムネイルに設定する。
  2. アルバムの公開設定が変更された場合に、その変更を Slack 通知で知らせる。

コードは以下のようになっています:

album.rb
class Album < ApplicationRecord
  after_save :set_thumbnail!, if: -> { saved_change_to_visible? }

  after_commit :send_notifications_on_changed_visibility, if: -> { saved_change_to_visible? }

  validates :visible, presence: true

  # アルバムが公開された時、一枚目の写真をサムネイルにする
  def set_thumbnail!
    return if !self.visible?

    self.update!(thumbnail_photograph_id: self.photographs.first)
  end

  # アルバムの公開設定が変更されたことを Slack で通知
  def send_notifications_on_changed_visibility
    # Slack通知
  end
end

この状態で、 album.update!(visible: true) を実行すると、どのような挙動になるでしょうか?

期待される動作は以下の通りです:

  • set_thumbnail! メソッドが呼び出され、一枚目の写真がサムネイルとして設定される。
  • send_notifications_on_changed_visibility メソッドが呼び出され、Slack 通知が送信される。

しかし、実際にはサムネイルが設定されるだけで、Slack 通知は送信されません。
この動作は一見すると不思議に思えるかもしれませんが、その原因は Dirty の仕組みにあります。

set_thumbnail! メソッド内で update! を実行することで、 saved_change_to_visible? の状態がリセットされてしまうため、Slack 通知用のメソッドが呼び出されないのです。

Rails Console を使った挙動の検証
この問題をさらに詳しく見てみましょう。以下は Rails Console で Dirty の状態を確認した結果です。

rails console
irb(dev):001 (00:00:01)> album = Album.last
=>
#<Album:0x0000fffffscjgi0
...

irb(dev):002 (00:00:02)> album.visible
=> false

irb(dev):003 (00:00:03)> album.visible = true
=> true

irb(dev):004 (00:00:04)> album.will_save_change_to_visible?
=> true

irb(dev):005 (00:00:05)> album.changes_to_save
=> {"visible"=>[nil, true]}

irb(dev):006 (00:00:06)> album.save
=> true

irb(dev):007 (00:00:07)> album.visible
=> true # visible が更新されているものの、、

irb(dev):008 (00:00:08)> album.saved_change_to_visible?
=> false # Dirty では visible が更新されていない扱い

irb(dev):009 (00:00:09)> album.saved_changes
=> {"eyecatch_photograph_id"=>[nil, 123456789]} # eyecatch_photograph_id しか更新されていないことになっている

この結果からも分かる通り、 set_thumbnail! メソッド内で update! を実行したタイミングで、 Dirty の変更検知がリセット されてしまいます。
そのため、 saved_change_to_visible?false を返し、Slack 通知の処理が実行されないという挙動になっているのです。

このように、処理内で更新を複数回おこなっている場合、 Dirty を用いた変更検知は意図しない結果を招くことがあります。
(弊社のコードではモデルに include している module 内に、コールバックによる update! が書かれていたため、発見が遅れました。。)

罠を回避する方法

弊社では Dirty に依存せず、安全に属性の変更前後を追跡する方法を取ることとしました。
保存される前に、使用したいカラムの値を old_XXX のように保持しておき、更新後の値と比較することで変更を検知する方法です。

これにより、 saved_changes の不安定な挙動に依存せず、より安全な処理を実現できます。

まとめ

ActiveRecord::AttributeMethods::Dirty は非常に便利な機能ですが、その内部動作を理解せずに使用すると、特にコールバックとの組み合わせでトラブルを引き起こすことがあります。

本記事では、 saved_changes の挙動とその罠について紹介しました。
Dirty に依存しすぎず必要な情報を明示的に保存して活用することで、安全な処理を書いていきましょう。

ラブグラフのエンジニアブログ

Discussion