0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第16章|今さら学ぶ「スコープとコールバック」

0
Last updated at Posted at 2026-02-23

第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の savedestroy は内部で決まった順番のステップを踏みます。before_saveafter_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次情報リンク)


まとめ

  • ✅ スコープは「検索ショートカット」。lambdaで定義し、メソッドチェーンで組み合わせ可能
  • ✅ default_scope は全クエリに暗黙適用されるため、基本的に避ける
  • ✅ コールバックは「自動ドア」。便利だが、増えすぎると見えない処理が増えて危険
  • ✅ enumで「下書き/公開/非公開」を整数管理。?/! メソッドとスコープが自動生成
  • ✅ 複雑なコールバックはServiceクラスやActive Jobに切り出すのがベスト

📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?