你有一个模型,比如说,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
,那也不起作用,因为它需要包含在类中。
在不将更多内容填充到原始类中的情况下对模型实例进行验证的正确方法是什么?
这个问题有几种解决方案。 最好的可能取决于您的特定需求。 下面的示例将使用此简单模型:
class Record
include ActiveModel::Validations
validates :value, presence: true
attr_accessor :value
end
仅导轨 4
使用singleton_class
ActiveSupport::回调在 Rails 4 中进行了彻底的修改,现在对singleton_class进行验证将起作用。 这在 Rails 3 中是不可能的,因为实现直接引用了self.class
。
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"]
导轨 3 和 4
在 Rails 3 中,验证也是使用 ActiveSupport::Callback 实现的。 回调存在于类上,虽然回调本身是在可以在实例级别重写的类属性上访问的,但利用这一点需要编写一些非常依赖于实现的粘合代码。 此外,"validates"和"validate"方法是类方法,所以你基本上需要一个类。
使用子类
这可能是 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 对象,有几种方法可以将超类对象"强制转换"到子类。subclass_record = record.becomes(subclass)
是一种方式。
请注意,这也将保留类方法validators
和validators_on(attribute)
。 例如,SimpleForm gem 使用这些来测试是否存在将"必需"CSS 类添加到相应表单字段的PresenceValidator
。
使用验证上下文
验证上下文是 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,如果你需要组合并且有太多的组合以至于子类是不切实际的,这可能是你最好的选择。 您可以使用多个模块进行扩展。
使用简单委托器
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"]
我在这里使用了一个匿名类来给出一个使用"一次性"类的示例。 如果您有足够动态的验证,使得定义良好的子类是不切实际的,但您仍然想使用"validate/validates"类宏,则可以使用 Class.new 创建一个匿名类。
您可能不想做的一件事是创建原始类的匿名子类(在这些示例中为 Record 类),因为它们将被添加到超类的 DescendantTracker 中,并且对于长期存在的代码,可能会给垃圾回收带来问题。
您可以在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)
,但我还没有测试过。
您是否考虑过使用关注点? 所以这样的事情应该有效。
module HasWinterTires
extend ActiveSupport::Concern
module ClassMethods
validates :speed, :inclusion => { :in => 0..30 }
end
end
这种担忧不会在乎它本身不知道validates
做什么。
那么我相信你可以做instance.extend(HasWinterTires)
,它应该都有效。
我是根据记忆写出来的,所以如果您有任何问题,请告诉我。
你可能想要这样的东西,这与你最初的尝试非常相似:
module HasWinterTires
def self.included(base)
base.class_eval do
validates :speed, :inclusion => { :in => 0..30 }
end
end
end
然后,该行为应该在您的示例中按预期工作,尽管您需要在 HasWinterTire 上使用包含而不是扩展。