mcpField
A
active-record-callbacks

active-record-callbacks

github.com/thinkoodle開発7188 回インストール8 週間前に更新
リポジトリ

active-record-callbacks について

Active Record コールバックを扱う Rails 開発者向けのスキルです。ライフサイクルやフックの選び方を説明し、モデル固有の処理とサービスオブジェクト向けの処理を区別して、トランザクション後の副作用も扱います。

主な機能

  1. 01コールバックに適した処理の判断
  2. 02作成・更新・削除の順序の把握
  3. 03適切なコールバックフックの選択
  4. 04コミット後の副作用の安全な処理
  5. 05コールバック連鎖の制御やスキップ

活用シーン

  1. 01モデルコールバックの追加や変更
  2. 02コールバック順序や挙動の調査
  3. 03コミット後の外部副作用の実行
  4. 04コールバックとサービスオブジェクトの使い分け

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill active-record-callbacks

インストール後、Claude Code で/active-record-callbacksを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Active Record コールバック

Active Record オブジェクトのライフサイクルにフックします。ただし、適切な場合に限ります。

このスキルを使う場面

  • モデルのコールバック(before_save、after_create、after_commit など)を追加または変更するとき
  • ロジックをコールバックに置くか、サービスオブジェクトに置くか判断するとき
  • 予期しないコールバックの動作や実行順序の問題をデバッグするとき
  • トランザクションに安全な副作用(after_commit パターン)を実装するとき
  • コールバックチェーンを停止したり、コールバックを抑制したりするとき

黄金律

コールバックを使うのは、モデル固有の関心事に限ります。 そのロジックをカラムのデフォルト値やデータベース制約として実装するのが自然でないなら、コールバックに置くべきではありません。

✅ コールバックを使う場面
  • デフォルト値の設定 — before_validation :set_default_role
  • データの正規化 — before_save :strip_whitespace、before_save :downcase_email
  • 値の導出 — before_save :calculate_total_from_line_items
  • トークンやスラッグの生成 — before_create :generate_uuid
  • カウンターキャッシュの維持 — Rails の組み込み機能では不十分な場合
  • タイムスタンプの更新 — updated_at のシンプルな伝播
  • 所有するリソースのクリーンアップ — after_destroy_commit :delete_file_from_storage
❌ コールバックを使わない場面
  • メールの送信 → サービスオブジェクト、または最低限 after_commit とジョブを使用
  • 外部 API の呼び出し → サービスオブジェクト
  • 別のレコードの作成 → サービスオブジェクト
  • 複雑なビジネスロジック → サービスオブジェクト
  • コンテキストに応じた処理 → サービスオブジェクト(コールバックには、保存が呼び出された理由がわかりません)
  • ジョブのエンキュー → after_commit を使い、after_save は使わないでください(データがまだコミットされていない可能性があります)
  • 認可チェック → コントローラー/ポリシーレイヤー
  • ビジネスイベントのログ記録 → サービスオブジェクト、または after_commit
重要な理由

コールバックは、Rails のスパゲッティコードの最大の原因です。コールバックは次の問題を引き起こします。

  • 実行させたくない場合でも、保存のたびに実行されます
  • 見えない結合を生みます(User を保存しただけでメールが送信されるなど)
  • デバッグを悪夢のようにします(コールバックチェーンを追跡するのは困難です)
  • 一括操作で機能しません(update_all では完全にスキップされます)
  • テストを不安定にします(レコードを保存するすべてのテストで、チェーン全体が実行されます)

迷ったら、サービスオブジェクトを使ってください。 その処理が本当にモデル固有の関心事なら、後からコールバックに切り出すこともできます。コールバックから処理を切り出すのは、はるかに困難です。

ライフサイクルの順序

レコードの作成
before_validation
after_validation
before_save
around_save
  before_create
  around_create
    [INSERT]
  around_create (after yield)
  after_create
around_save (after yield)
after_save
after_commit / after_rollback
レコードの更新
before_validation
after_validation
before_save
around_save
  before_update
  around_update
    [UPDATE]
  around_update (after yield)
  after_update
around_save (after yield)
after_save
after_commit / after_rollback
レコードの削除
before_destroy
around_destroy
  [DELETE]
around_destroy (after yield)
after_destroy
after_commit / after_rollback

重要なポイント: before_save/after_save は、作成と更新の両方を囲みます。より具体的な before_create/after_create または before_update/after_update の前後には、必ず実行されます。

手順

ステップ 1:「これはコールバックにするべきか?」と考える

モデル固有性のテストを適用します。そのレコードが「どこで」または「なぜ」保存されるかにかかわらず、このロジックを実行する必要がありますか?

  • メールアドレスの正規化? はい — 常に小文字にする → コールバック
  • ウェルカムメールの送信? いいえ — サインアップ時だけで、管理者による編集時には送らない → サービスオブジェクト
  • UUID の設定? はい — 常に必要 → コールバック
  • Slack への通知? いいえ — コンテキストによる → サービスオブジェクト

「〜の場合に限る」や「〜の場合を除く」という条件が含まれるなら、おそらくコールバックにするべきではありません。条件付きコールバック(if:/unless:)は、そのロジックがコールバックに適していないことを示すコードスメルです。

ステップ 2:適切なフックを選ぶ
必要なことフック理由
バリデーション前にデフォルト値を設定するbefore_validation値がバリデーションで利用できます
データを正規化/導出するbefore_save作成時と更新時の両方に実行されます
初回作成時のみ値を設定するbefore_create更新時には実行されません
DB 書き込み後(トランザクション内)に処理するafter_save / after_create / after_updateデータは保存済みですが、トランザクションがロールバックされる可能性があります
DB コミット後(安全)に処理するafter_commit / after_create_commitトランザクションがコミット済みなので、ジョブや外部呼び出しにも安全です
削除時に後処理するafter_destroy_commitレコードが完全に削除されています
保存/破棄を防ぐbefore_save / before_destroy + throw :abortコールバックチェーンを停止します

重要なルール: コールバックからデータベース外の何か(API、ジョブキュー、ファイルシステム、メーラー)にアクセスする場合は、after_save ではなく after_commit を使ってください。after_save 内のデータはロールバックされる可能性があります。

ステップ 3:登録

マクロ形式の登録では、プライベートメソッドを優先してください:

class User < ApplicationRecord
  before_validation :normalize_email
  before_create :generate_api_token

  private

  def normalize_email
    self.email = email&.strip&.downcase
  end

  def generate_api_token
    self.api_token = SecureRandom.hex(32)
  end
end

ブロックは本当に一行で済む場合にのみ使ってください:

class User < ApplicationRecord
  before_save { self.email = email&.downcase }
end

ラムダ/proc 形式は使わないでください。読みづらく、テストもしにくくなります:

# Don't do this
before_save ->(user) { user.email = user.email&.downcase }
ステップ 4:条件付きコールバック(必要最小限にする)

条件付きコールバックが必要な場合は、シンボルを使った :if / :unless を優先してください:

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
  before_save :apply_discount, unless: :promotional?
end

複数の条件(すべてを満たす必要があります):

before_save :archive_data, if: [:completed?, :older_than_90_days?]

コードスメルに注意: 条件が複数ある場合や、proc 条件が複雑な場合、そのロジックはほぼ間違いなくサービスオブジェクトに置くべきです。

ステップ 5:コールバックチェーンの停止

操作を防ぐには throw :abort を使ってください:

class Account < ApplicationRecord
  before_destroy :ensure_no_active_subscriptions

  private

  def ensure_no_active_subscriptions
    if subscriptions.active.any?
      errors.add(:base, "Cannot delete account with active subscriptions")
      throw :abort
    end
  end
end

throw :abort の動作:

  • save → false を返します
  • save! → ActiveRecord::RecordNotSaved を発生させます
  • destroy → false を返します
  • destroy! → ActiveRecord::RecordNotDestroyed を発生させます

実行を停止するために、コールバック内で例外を発生させないでください。 throw :abort を使ってください。

ステップ 6:トランザクションコールバック(after_commit)

Rails で最も重要なコールバックの違い:

フック実行されるタイミングトランザクションの状態外部呼び出しに安全か
after_saveINSERT/UPDATE の後トランザクション中❌ いいえ — ロールバックされる可能性があります
after_commitCOMMIT の後コミット済み✅ はい
after_rollbackROLLBACK の後ロールバック済み該当なし

便利なエイリアス(こちらを優先してください):

class User < ApplicationRecord
  after_create_commit :send_welcome_email_job
  after_update_commit :sync_to_crm_job
  after_destroy_commit :cleanup_storage
  after_save_commit :broadcast_changes  # create OR update
end

注意点 — 同じメソッド名での after_create_commit + after_update_commit:

# BUG: Only after_update_commit will fire — they alias to after_commit internally
after_create_commit :log_change
after_update_commit :log_change  # This overwrites the create one!

# FIX: Use after_save_commit for both
after_save_commit :log_change

注意点 — after_commit はトランザクションごとにレコードあたり 1 回実行されます:

User.transaction do
  user.update!(name: "First")
  user.update!(name: "Second")
end
# after_commit fires ONCE, not twice
ステップ 7:コールバックのスキップ

以下のメソッドはコールバックを完全に回避します:

  • update_column / update_columns — 単一レコードを更新し、コールバック/バリデーションは実行しません
  • update_all — 一括更新し、コールバックは実行しません
  • delete / delete_all / delete_by — コールバックは実行しません(コールバックが実行される destroy とは異なります)
  • insert / insert_all / upsert / upsert_all — 一括挿入し、コールバックは実行しません
  • increment! / decrement! — カウンターを更新し、コールバックは実行しません
  • touch_all — 一括タッチし、コールバックは実行しません

カウンター更新や一括データ移行など、コールバックが不要だと分かっている場合に、意図的に使ってください。

ステップ 8:around_* コールバック

yield で操作をラップします:

class User < ApplicationRecord
  around_save :benchmark_save

  private

  def benchmark_save
    start = Time.current
    yield  # The actual save happens here
    duration = Time.current - start
    Rails.logger.info("User#save took #{duration}s")
  end
end

操作をラップする必要が本当にある場合(ベンチマークやカスタムのトランザクション処理など)を除き、around_* コールバックは避けてください。 複雑さが増し、挙動も把握しにくくなります。

ステップ 9:コールバックオブジェクト(再利用する場合)

同じコールバックロジックを複数のモデルに適用する場合は、クラスに切り出します:

class NormalizeBlankToNil
  def self.before_save(record)
    record.attributes.each do |key, value|
      record[key] = nil if value.is_a?(String) && value.blank?
    end
  end
end

class User < ApplicationRecord
  before_save NormalizeBlankToNil
end

class Post < ApplicationRecord
  before_save NormalizeBlankToNil
end

クラスメソッド(上記)はインスタンス化する必要がありません。コールバックオブジェクト自体が状態を持つ必要がある場合にのみ、インスタンスメソッドを使ってください。

ステップ 10:dependent: を指定する場合の before_destroy の順序
class User < ApplicationRecord
  # ⚠️ WRONG ORDER — children deleted before callback runs
  has_many :posts, dependent: :destroy
  before_destroy :check_can_delete

  # ✅ CORRECT — use prepend: true so callback runs first
  has_many :posts, dependent: :destroy
  before_destroy :check_can_delete, prepend: true

  # ✅ ALSO CORRECT — declare callback before association
  before_destroy :check_can_delete
  has_many :posts, dependent: :destroy
end

アンチパターン

1. ゴッドコールバック
# ❌ TERRIBLE — does way too much
after_create :setup_everything

def setup_everything
  send_welcome_email
  create_default_workspace
  sync_to_stripe
  notify_admin_slack
  enqueue_onboarding_drip
end

→ 代わりに UserRegistrationService を使ってください。

2. 条件分岐のスパゲッティ
# ❌ Code smell — context-dependent logic
after_save :maybe_notify, if: -> { 
  saved_change_to_status? && status == "published" && !importing? && !admin_edit? 
}

→ 通知を明示的に呼び出す PublishPostService に移してください。

3. コールバック駆動型アーキテクチャ
# ❌ "Saving a record" triggers a Rube Goldberg machine
after_save :update_cache
after_save :recalculate_totals
after_save :sync_search_index
after_save :notify_subscribers
after_save :update_analytics

→ これらのほとんどは、サービスオブジェクトに含めるか、明示的にトリガーされる非同期ジョブにするのが適切です。

4. 外部呼び出しに after_save を使用する
# ❌ DANGEROUS — transaction may rollback after email is sent
after_save :send_notification_email

# ✅ SAFE — only fires after commit
after_commit :send_notification_email, on: :create

コールバックの抑制

ActiveRecord::Suppressor を使用すると、特定のモデルの保存を一時的に抑制できます。

Notification.suppress do
  User.create!(name: "Test") # Won't create notifications even if after_create does
end

使用は必要最小限にしてください — これはテストやシードデータ作成のためのツールであり、本番環境のロジックではありません。

参考資料

詳しいパターン、例、エッジケース、コールバックのライフサイクル全体については、このスキルディレクトリの reference.md を参照してください。

コメント