active-record-callbacks について
Active Record コールバックを扱う Rails 開発者向けのスキルです。ライフサイクルやフックの選び方を説明し、モデル固有の処理とサービスオブジェクト向けの処理を区別して、トランザクション後の副作用も扱います。
主な機能
- 01コールバックに適した処理の判断
- 02作成・更新・削除の順序の把握
- 03適切なコールバックフックの選択
- 04コミット後の副作用の安全な処理
- 05コールバック連鎖の制御やスキップ
活用シーン
- 01モデルコールバックの追加や変更
- 02コールバック順序や挙動の調査
- 03コミット後の外部副作用の実行
- 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_save | INSERT/UPDATE の後 | トランザクション中 | ❌ いいえ — ロールバックされる可能性があります |
after_commit | COMMIT の後 | コミット済み | ✅ はい |
after_rollback | ROLLBACK の後 | ロールバック済み | 該当なし |
便利なエイリアス(こちらを優先してください):
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 を参照してください。