如何将验证放在单个ActiveModel / ActiveRecord对象上?

时间:2014-01-03 18:44:11

标签: ruby-on-rails validation activerecord activemodel

你有一个模型,比如Car。某些验证始终适用于每个Car实例:

class Car
  include ActiveModel::Model

  validates :engine, :presence => true
  validates :vin,    :presence => true
end

但是某些验证仅在特定情况下相关,因此只有某些实例才应具有这些验证。你想在某个地方这样做:

c = Car.new
c.extend HasWinterTires
c.valid?

这些验证转到其他地方,进入另一个模块:

module HasWinterTires
  # Can't go fast with winter tires.
  validates :speed, :inclusion => { :in => 0..30 }
end

如果您这样做,validates将失败,因为它未在Module中定义。如果添加include ActiveModel::Validations,则无法使用,因为它需要包含在类中。

在模型实例上放置验证的正确方法是什么,而不会在原始类中填充更多内容?

4 个答案:

答案 0 :(得分:2)

这个问题有几种解决方案。最好的可能取决于您的特殊需求。以下示例将使用此简单模型:

class Record
  include ActiveModel::Validations

  validates :value, presence: true

  attr_accessor :value
end

仅限Rails 4

使用singleton_class

ActiveSupport ::回调在Rails 4中彻底改革,并且对singleton_class进行验证现在可以正常工作。由于直接引用self.class

,因此在Rails 3中无法做到这一点
record = Record.new value: 1
record.singleton_class.validates :value, numericality: {greater_than: 1_000_000}
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

Rails 3和4

在Rails 3中,还使用ActiveSupport :: Callbacks实现了验证。回调存在于类中,虽然回调本身是在类属性上访问的,可以在实例级重写,但利用这一点需要编写一些非常依赖于实现的粘合代码。此外,"验证"和"验证"方法是类方法,所以你基本上需要一个类。

使用子类

除非您需要可组合性,否则这可能是Rails 3中的最佳解决方案。您将从超类继承基本验证。

class BigRecord < Record
  validates :value, numericality: {greater_than: 1_000_000}
end

record = BigRecord.new value: 1
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

对于ActiveRecord对象,有几种方法可以实现&#34; cast&#34;子类的超类对象。 subclass_record = record.becomes(subclass)是单向的。

请注意,这还会保留类方法validatorsvalidators_on(attribute)。例如,SimpleForm gem使用这些来测试是否存在PresenceValidator来添加&#34; required&#34; CSS类到相应的表单字段。

使用验证上下文

验证上下文是&#34;官方&#34; Rails对同一类的对象进行不同验证的方法。不幸的是,验证只能在一个上下文中进行。

class Record
  include ActiveModel::Validations

  validates :value, presence: true

  attr_accessor :value

  # This can also be put into a module (or Concern) and included
  with_options :on => :big_record do |model|
    model.validates :value, numericality: {greater_than: 1_000_000}
  end
end

record = Record.new value: 1
record.valid?(:big_record) || record.errors.full_messages
# => ["Value must be greater than 1000000"]

# alternatively, e.g., if passing to other code that won't supply a context:
record.define_singleton_method(:valid?) { super(:big_record) }
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

使用#validates_with实例方法

#validates_with是唯一可用于验证的实例方法之一。它接受一个或多个验证器类和任何选项,它们将传递给所有类。它将立即实例化类并将记录传递给它们,因此需要在调用#valid?时运行。

module Record::BigValidations
  def valid?(context=nil)
    super.tap do
      # (must validate after super, which calls errors.clear)
      validates_with ActiveModel::Validations::NumericalityValidator,
        :greater_than => 1_000_000,
        :attributes => [:value]
    end && errors.empty?
  end
end

record = Record.new value: 1
record.extend Record::BigValidations
record.valid? || record.errors.full_messages
# => ["Value must be greater than 1000000"]

对于Rails 3,如果你需要组合并且有很多组合使得子类不切实际,这可能是你最好的选择。您可以使用多个模块进行扩展。

使用SimpleDelegator

big_record_delegator = Class.new(SimpleDelegator) do
  include ActiveModel::Validations

  validates :value, numericality: {greater_than: 1_000_000}

  def valid?(context=nil)
    return true if __getobj__.valid?(context) && super
    # merge errors
    __getobj__.errors.each do |key, error|
      errors.add(key, error) unless errors.added?(key, error)
    end
    false
  end

  # required for anonymous classes
  def self.model_name
    Record.model_name
  end
end

record = Record.new value: 1
big_record = big_record_delegator.new(record)
big_record.valid? || big_record.errors.full_messages
# => ["Value must be greater than 1000000"]

我在这里使用了一个匿名类来举例说明使用&#34;一次性&#34;类。如果你有足够的动态验证,那么明确定义的子类是不切实际的,但你仍然想要使用&#34;验证/验证&#34;类宏,您可以使用Class.new创建一个匿名类。

您可能不想做的一件事是创建原始类的匿名子类(在这些示例中,Record类),因为它们将被添加到超类的DescendantTracker中,并且很长时间生成的代码,可能会给垃圾收集带来问题。

答案 1 :(得分:0)

如果Car Car extends模块,您可以在HasWinterTires上执行验证...例如:

class Car < ActiveRecord::Base

  ...

  validates :speed, :inclusion => { :in => 0..30 }, :if => lambda { self.singleton_class.ancestors.includes?(HasWinterTires) }

end

我认为你可以self.is_a?(HasWinterTires)而不是singleton_class.ancestors.include?(HasWinterTires),但我还没有测试过。

答案 2 :(得分:0)

你有没有考虑使用关注?所以这样的事情应该有效。

module HasWinterTires
  extend ActiveSupport::Concern

  module ClassMethods
    validates :speed, :inclusion => { :in => 0..30 }
  end
end

关注点并不在乎它本身不知道validates做了什么。

然后我相信你可以做instance.extend(HasWinterTires),它应该都可以。

我是从记忆中写出来的,所以如果你有任何问题,请告诉我。

答案 3 :(得分:0)

你可能想要这样的东西,这与你原来的尝试非常相似:

module HasWinterTires
  def self.included(base)
    base.class_eval do
      validates :speed, :inclusion => { :in => 0..30 }
    end
  end
end

然后,行为应该在您的示例中按预期工作,但您需要在HasWinterTires上使用include而不是extend。