QA@IT

ActiveRecord でオブジェクト毎に Validator を差し込むことはできますか

2585 PV

ActiveRecord でオブジェクト毎に異なる validation を行いたいのですが、例えば以下のようにしても validation を追加することができません。何かよい方法は無いでしょうか?

class User < ActiveRecord::Base; end

user = User.new
user.validates :bio, presence: true # NoMethodError: undefined method `validates' for #<User:0x007ff12773a648>

回答

直接の回答とは異なりますが、validationというかコールバックの:if`オプションをうまいことやれば、似たようなことを実現できそうです。

たとえば、コールバックコンテキストをスレッド変数でもっておき、以下の様な立て付けで定義しておきます。こうすると、with_contextのブロックの中でのみバリデーションが実行されるわけです。

module ValidationContexts
  def validation_context(context, &block)
    key = ValidationContexts.to_key(context)
    self.with_options(if: -> { Thread.current[key] }, &block)
  end

  def with_context(context)
    begin
      Thread.current[ValidationContexts.to_key(context)] = true
      yield
    ensure
      Thread.current[ValidationContexts.to_key(context)] = nil
    end
  end

  def self.to_key(context)
    "__validation_context_#{context}__".to_sym
  end
end

というモジュールがあったとして、アプリケーションコードに下記のようにしておきます。

class User < ActiveRecord::Base
  extend ValidationContexts

  validation_context(:foo) do |ctxt|
    ctxt.validates :email, presence: true
  end
end

実行時に、バリデーションのコンテキストを切り替えるには次のようにします。

u = User.new(email: nil)
p u.valid? # => true

User.with_context :foo do
  p u.valid? # => false
end

これで、(手動ですが)バリデーションの実行コンテキストを良い感じに制御できているのではないでしょうか。

また、これをアプリケーションに組み込みたい、例えばRailsAdminの場合のみ〜などやりたい場合は、下記のようにAC.around_filterと組み合わせてはいかがでしょうか。

class ApplicationController < ActionController::Base
   # これで、RailsAdminの時だけに確実に効かせる
  def self.inherited(klass)
    if klass.name == "RailsAdmin::MainController"
      klass.around_filter :admin_context
    end
  end

  def admin_context(&action)
    User.with_context(:admin, &action)
  end
end
編集 履歴 (1)
  • ありがとうございます。なるほど、スレッドをひとつのコンテキストとして扱うというのはいいですね! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。