第16章|今さら学ぶ「スコープとコールバック」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- スコープ(scope)— よく使う検索条件に名前をつける
- default_scope の注意点
- コールバック(before_save / after_create 等)— 保存前後の自動処理
- コールバックの落とし穴 — 「見えない処理」が増える危険
- enum — 「下書き・公開」を整数で管理し、スコープも自動生成
🏠 たとえ話で掴む「スコープとコールバック」
スコープ は、図書館の 検索ショートカット です。毎回「棚Aの、2024年以降の、日本語の、貸出可能な本」と検索条件を入力するのは面倒です。よく使う条件に「新着の日本語本」と名前をつけてワンクリックで呼び出せるようにする——これがスコープです。
コールバック は、 自動ドア です。ドアの前に立つ(保存する)と、自動で開く(前処理が実行される)。通り過ぎた後に自動で閉まる(後処理が実行される)。便利ですが、自動ドアが増えすぎると「いつどのドアが開くか」がわからなくなります。
📖 スコープとコールバックの技術的な定義
スコープとは
スコープ(scope) は、よく使う検索条件を lambda(無名関数)としてクラスメソッドに登録する仕組み です。
scope :published, -> { where(published: true) } と書くと、内部的には Article.published というクラスメソッドが定義されます。重要なのは、スコープが返すのは ActiveRecord::Relation オブジェクトであるという点です。Relationオブジェクトはさらにスコープやwhereをチェーンできるため、Article.published.recent.by_user(1) のように条件を自由に組み合わせられます。
コールバックとは
コールバック は、Active Recordオブジェクトの ライフサイクル(作成・更新・削除など)の各タイミングに処理を差し込むフック です。
Active Recordの save や destroy は内部で決まった順番のステップを踏みます。before_save や after_create はそのステップの「前」や「後」に自動で呼ばれるメソッドを登録する宣言です。データの前処理(空白除去、デフォルト値設定)や後処理(通知作成、キャッシュ更新)を、コントローラではなくモデルに閉じ込められるのが利点です。
🔍 スコープ — 検索条件のショートカット
基本の使い方
class Article < ApplicationRecord
# lambda で検索条件を定義する
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
scope :recent, -> { order(created_at: :desc) }
# 引数つきスコープ
scope :by_user, ->(user_id) { where(user_id: user_id) }
scope :created_after, ->(date) { where("created_at >= ?", date) }
end
# 使い方 — メソッドチェーンで組み合わせ自在
Article.published # 公開済み記事
Article.published.recent # 公開済み × 新しい順
Article.published.recent.by_user(1) # + ユーザー1の記事
Article.published.created_after(1.week.ago) # 1週間以内の公開記事
スコープの正体は -> { } (lambda)です(→ 第4章)。クラスメソッドとして定義されるので、メソッドチェーンが可能になります。
スコープ vs クラスメソッド
# スコープで書く
scope :published, -> { where(published: true) }
# クラスメソッドで書いても同じ
def self.published
where(published: true)
end
どちらでも動きますが、 短い条件はスコープ、複雑なロジックはクラスメソッド で書くのが一般的です。
default_scope — 使うなら慎重に
class Article < ApplicationRecord
default_scope { order(created_at: :desc) } # 常に新しい順
end
# 全てのクエリに自動適用される
Article.all # => ORDER BY created_at DESC が常につく
Article.published # => WHERE published = true ORDER BY created_at DESC
default_scope は便利そうに見えますが、 全てのクエリに暗黙で適用されるため、予期しない挙動の原因になります。
# default_scope の罠
Article.all.to_sql
# => "SELECT * FROM articles ORDER BY created_at DESC"
# ↑ order がいつの間にかついている!
# 解除するには unscoped が必要
Article.unscoped.all
実務では default_scope を避け、明示的にスコープを呼び出すのが安全です。
⚙️ コールバック — 保存前後の自動処理
よく使うコールバック
class Article < ApplicationRecord
before_validation :normalize_title # バリデーション前
before_save :set_published_at # 保存前
after_create :notify_followers # 新規作成後
after_destroy :cleanup_tags # 削除後
private
def normalize_title
self.title = title&.strip # 前後の空白を除去
end
def set_published_at
if published_changed? && published?
self.published_at = Time.current
end
end
def notify_followers
# フォロワーに通知を作成(→ [第29章](https://qiita.com/harapeco-mgn/items/420a7d80595504b0444d)で非同期化)
user.followers.each do |follower|
Notification.create(user: follower, notifiable: self, action_type: "published")
end
end
def cleanup_tags
# 使われなくなったタグを削除
tags.each { |tag| tag.destroy if tag.articles.empty? }
end
end
コールバックの実行順序
before_validation → validation → after_validation
→ before_save → before_create(新規)or before_update(更新)
→ DBに保存
→ after_create or after_update → after_save
→ after_commit(トランザクション完了後)
コールバックの落とし穴
コールバックは便利ですが、 増えすぎると「見えない処理」だらけになります。
# ❌ コールバックが多すぎるモデル
class Article < ApplicationRecord
before_save :normalize_title
before_save :generate_slug
before_save :set_published_at
after_create :notify_followers
after_create :send_email
after_create :update_user_stats
after_save :clear_cache
after_destroy :cleanup_tags
after_destroy :update_search_index
# → article.save するだけで裏側で9つの処理が走る!
end
article.save とだけ書いているのに裏側で大量の処理が走り、「なぜ遅いのか」「なぜ予期しない通知が飛ぶのか」がわかりにくくなります。
対策: コールバックは最小限に。複雑な後処理はService Object(→ 第25章)やActive Job(→ 第29章)に切り出すのが定石です。
🏷️ enum — 状態を整数で管理する
enum は、「下書き」「公開」「非公開」のような状態を 整数でDBに保存しつつ、Rubyでは名前で扱える 仕組みです。
マイグレーションで status カラムを追加する
enum を使うには、まず整数型のカラムが必要です。
$ rails generate migration AddStatusToArticles status:integer
# db/migrate/20250215130000_add_status_to_articles.rb
class AddStatusToArticles < ActiveRecord::Migration[8.0]
def change
add_column :articles, :status, :integer, default: 0, null: false
# ↑ default: 0 で、新規レコードは自動的に「draft」になる
end
end
enum の定義と自動生成メソッド
class Article < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2 }
end
article = Article.new(status: :draft)
# 状態の確認(?つきメソッドが自動生成)
article.draft? # => true
article.published? # => false
# 状態の変更(!つきメソッドが自動生成)
article.published! # => statusを1(published)に変更して保存
# スコープも自動生成!
Article.draft # => 下書き記事だけ
Article.published # => 公開記事だけ
Article.archived # => アーカイブ記事だけ
DBには整数で保存される
articles テーブル
+----+----------+--------+
| id | title | status |
+----+----------+--------+
| 1 | Ruby入門 | 0 | ← 0 = draft
| 2 | Rails基礎| 1 | ← 1 = published
+----+----------+--------+
整数で保存することで、文字列より省メモリかつ高速に検索できます。
prefix / suffix — スコープ名の衝突を防ぐ
モデルに複数の enum を定義すると、自動生成されるスコープ名が衝突することがあります。prefix オプションで回避できます。
class Article < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2 }, prefix: true
enum :visibility, { visible: 0, hidden: 1 }, prefix: true
end
# prefix: true をつけると、メソッド名にカラム名がつく
article.status_draft? # article.draft? の代わり
article.status_published! # article.published! の代わり
Article.status_published # Article.published の代わり
article.visibility_visible?
article.visibility_hidden!
1つの enum だけなら prefix は不要ですが、複数の enum を持つモデルでは衝突防止のためにつけておくと安全です。
🛠️ KnowledgeNoteでの具体例
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
enum :status, { draft: 0, published: 1, archived: 2 }
scope :recent, -> { order(created_at: :desc) }
scope :by_tag, ->(name) { joins(:tags).where(tags: { name: name }) }
scope :feed_for, ->(user) { where(user_id: user.following_ids).published }
before_save :normalize_title
private
def normalize_title
self.title = title.strip if title.present?
end
end
# app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :user
belongs_to :notifiable, polymorphic: true
scope :unread, -> { where(read: false) }
scope :recent, -> { order(created_at: :desc) }
end
💼 面接で聞かれたら?
Q:スコープとコールバックについて説明してください。
「スコープはよく使う検索条件にlambdaで名前をつけて再利用する仕組みです。メソッドチェーンで組み合わせられるので、
Article.published.recentのように読みやすく書けます。コールバックは保存前後に自動実行される処理で、before_saveやafter_createなどがあります。ただしコールバックは増えすぎると処理の流れが見えにくくなるため、最小限に留め、複雑な処理はServiceクラスに切り出すのが良い設計です。」深掘りされたら:
- 「enumとは?」→ 文字列の状態をDB上では整数として保存し、Rubyでは名前でアクセスできる仕組み。
?メソッド、!メソッド、スコープが自動生成される。- 「default_scope の問題点は?」→ 全クエリに暗黙で適用されるため予期しない挙動を招く。意図しないORDERやWHERE条件がつき、デバッグが困難になる。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Active Record クエリインターフェイス(スコープ) — スコープの全オプション
- Rails ガイド:Active Record コールバック — コールバックの実行順序と全種類
- Rails API:ActiveRecord::Enum — enumの公式リファレンス
まとめ
- ✅ スコープは「検索ショートカット」。lambdaで定義し、メソッドチェーンで組み合わせ可能
- ✅ default_scope は全クエリに暗黙適用されるため、基本的に避ける
- ✅ コールバックは「自動ドア」。便利だが、増えすぎると見えない処理が増えて危険
- ✅ enumで「下書き/公開/非公開」を整数管理。
?/!メソッドとスコープが自動生成 - ✅ 複雑なコールバックはServiceクラスやActive Jobに切り出すのがベスト
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに