ややこしいタイトルだけど、Ruby on Rails のモデルコールバックで迷った箇所があったのでメモ。

A has_many(has_one) B な関係で、B モデルの callback 実行を以下のパターンで制御したい。

  • 親要素の A モデルと一緒に B モデルのデータが作成された時だけ callback を実行
  • 親要素の A モデルの dependent: :destroy で削除された時だけ callback を実行
  • 子要素の B モデルが単独で作成・更新された時だけ callback を実行
  • 子要素の B モデルが単独で削除された時だけ callback を実行

まずはモデルのシンプルな関係を示すコード。Article has_many Comment という感じ。

class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
end

class Comment < ApplicationRecord
  belongs_to :article
end

これを上記の4つのパターン別に callback を登録してみるとこんな感じ。

class Comment < ApplicationRecord
  belongs_to :article

  after_save_commit :created_by_association_only, if: :created_by_association?
  after_save_commit :created_by_self_only, unless: :created_by_association?

  after_destroy_commit :destroyed_by_association_only, if: :destroyed_by_association?
  after_destroy_commit :destroyed_by_self_only, unless: :destroyed_by_association?

  private

  def created_by_association?
    article.saved_change_to_updated_at?
  end

  def destroyed_by_association?
    destroyed_by_association.present?
  end

  def created_by_association_only
    logger.info('created_by_association_only')
  end

  def created_by_self_only
    logger.info('created_by_self_only')
  end

  def destroyed_by_association_only
    logger.info('destroyed_by_association_only')
  end

  def destroyed_by_self_only
    logger.info('destroyed_by_self_only')
  end
end

親要素から作られたか?

これを判定するメソッドが Rails には見当たらなかったので、親要素のオブジェクトが更新されているかどうかを確認して代替とすることにした。確認する方法は model.saved_change_to_カラム名?のマジックメソッドを使って、更新日が更新されているかどうかで判定している。

親要素が更新されていれば、親と一緒に作成されているし、親要素が更新されていなければ子要素が単独で作成・更新されている…という風に判断している。果たしてこれで問題がないのかは若干の不安があるが、基本的には問題がなさそう。

親要素と一緒に削除されたか?

削除に関しては destroyed_by_association という便利なメソッドが生えている。これは dependent: :destroy 指定で一緒に削除される際に、削除元となったモデル情報が格納されている。子要素が単独で削除された場合は nil になる。これを使えば、一緒に削除されたのか単独なのかが判断できる。

あとは callback の if オプションで条件を指定してあげれば4パターンそれぞれで callback を使い分けることができる。