你有一个模型,比如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
,则无法使用,因为它需要包含在类中。
在模型实例上放置验证的正确方法是什么,而不会在原始类中填充更多内容?
答案 0 :(得分:2)
这个问题有几种解决方案。最好的可能取决于您的特殊需求。以下示例将使用此简单模型:
class Record
include ActiveModel::Validations
validates :value, presence: true
attr_accessor :value
end
ActiveSupport ::回调在Rails 4中彻底改革,并且对singleton_class进行验证现在可以正常工作。由于直接引用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"]
在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)
是单向的。
请注意,这还会保留类方法validators
和validators_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
是唯一可用于验证的实例方法之一。它接受一个或多个验证器类和任何选项,它们将传递给所有类。它将立即实例化类并将记录传递给它们,因此需要在调用#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"]
我在这里使用了一个匿名类来举例说明使用&#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。