😸

Rails validation contextの活用事例

に公開

1. 結論(この記事で得られること)

この記事では、Railsの「validation context」を実務でどう使いこなすかを具体的に解説します。

得られるもの:

  • 「同一モデルで画面ごとにバリデーションを変えたい」という要件への実装パターン
  • contextを使うべき場面・使うべきでない場面の判断基準
  • テストコードを含めた実装例と、運用時の失敗パターン
  • AIツールを使った validation 設計の最速チェック方法

実は昔、私も「admin画面では必須だけどユーザー画面では任意」という要件で、「if」文まみれのバリデーションを書いて後悔した経験があります。contextを知っていればもっとシンプルに書けたんですよね。

2. 前提(環境・読者層)

対象読者:

  • Rails 5.x 〜 7.x で開発している方
  • モデルのバリデーションが複雑化してきて困っている方
  • 管理画面とユーザー画面で異なるバリデーションを実装したい方

検証環境:

Ruby: 3.2.2
Rails: 7.1.2

ただし、validation contextは Rails 3 からある機能なので、古めのプロジェクトでも使えます。

3. Before:よくあるつまずきポイント

3-1. 条件分岐地獄のバリデーション

こういうコード、見覚えありませんか?

class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
 
  # admin画面でのみ必須
  validates :department_id, presence: true, if: :admin_context?
  validates :employee_code, presence: true, if: :admin_context?
 
  # ユーザー登録時のみ必須
  validates :password, presence: true, if: :user_registration?
  validates :terms_accepted, acceptance: true, if: :user_registration?
 
  attr_accessor :context_type
 
  def admin_context?
    context_type == 'admin'
  end
 
  def user_registration?
    context_type == 'registration'
  end
end

これの何が問題か:

① インスタンス変数の管理が煩雑 → 「context_type」の設定漏れでバグる

② メソッドが増え続ける → 「admin_context?」, 「user_registration?」…

③ テストが書きづらい → 条件の組み合わせ爆発

3-2. 別モデルに切り出す誘惑

「だったら 「AdminUser」 と 「User」 でモデル分けるか…」という方向に行きがちですが、これも危険です。

# STIで分離
class User < ApplicationRecord
end
 
class AdminUser < User
  validates :department_id, presence: true
end
 
class NormalUser < User
  validates :terms_accepted, acceptance: true
end

問題点:

  • DBテーブル設計が複雑化(STI の type カラム管理)
  • ユーザーの種別変更が困難(一般→管理者への昇格など)
  • 本質的に「同じユーザー」なのに別クラスで扱う違和感

4. After:基本的な解決パターン

4-1. validation context の基本

実はRailsにはcontextという仕組みが最初から用意されています。

class User < ApplicationRecord
  # 常に必須
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
 
  # adminコンテキストでのみ必須
  validates :department_id, presence: true, on: :admin
  validates :employee_code, presence: true, 
            format: { with: /\A[A-Z]{2}\d{4}\z/ }, 
            on: :admin
 
  # registrationコンテキストでのみ必須
  validates :password, presence: true, 
            length: { minimum: 8 }, 
            on: :registration
  validates :terms_accepted, acceptance: true, on: :registration
end

使い方:

# 通常の保存(デフォルトのバリデーション+コンテキストなし)
user.save
 
# adminコンテキストで保存
user.save(context: :admin)
 
# registrationコンテキストで検証
user.valid?(:registration)

4-2. 複数コンテキストの指定

配列で複数指定も可能です。

class Article < ApplicationRecord
  # 通常保存とpublishの両方で必須
  validates :title, presence: true
  validates :body, presence: true
 
  # publish時のみ
  validates :published_at, presence: true, on: :publish
  validates :category_id, presence: true, on: :publish
 
  # 下書き保存時は緩め(タイトルだけ)
  validates :title, presence: true, on: :draft
end

コントローラーでの使い分け:

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
 
    context = params[:commit] == '公開' ? :publish : :draft
 
    if @article.save(context: context)
      redirect_to @article
    else
      render :new
    end
  end
end

4-3. いつ使うべきか・使わないべきか

使うべき場面:

  • 管理画面 vs ユーザー画面で要件が異なる
  • 下書き保存 vs 本番公開で厳密さが違う
  • ステップ入力(ウィザード形式)で段階的に検証

使わないべき場面:

  • 単なる条件分岐(「if: -> { status == 'active' }」で済むなら不要)
  • ビジネスロジックの分離(Formオブジェクト等の検討を)
  • 永続化しないフォーム(ActiveModel::Model の検討を)

私の経験則ですが、「保存するタイミング・画面によって要求が変わる」ならcontext、「データの状態によって要求が変わる」なら条件分岐という使い分けが良いです。


続きはnoteで

この記事の実装編・詳細解説はnoteで公開しています。

実際のコード例や、実務で遭遇するハマりポイントなど、より踏み込んだ内容を書いています。

Discussion