如何在单表inheritance中运行子类的validation?

在我的应用程序中,我有一个名为Budget的类。 预算可以有多种类型。例如,假设有两个预算:FlatRateBudget和HourlyRateBudget。 两者都inheritance自预算class。

这是我到目前为止所得到的:

class Budget < ActiveRecord::Base validates_presence_of :price end class FlatRateBudget < Budget end class HourlyRateBudget < Budget validates_presence_of :quantity end 

在控制台中,如果我这样做:

 b = HourlyRateBudget.new(:price => 10) b.valid? => false b.errors.full_messages => ["Quantity can't be blank"] 

正如所料。

问题是STI上的“类型”字段来自params ..所以我需要做类似的事情:

 b = Budget.new(:type => "HourlyRateBudget", :price => 10) b.valid? => true 

这意味着rails在超类中运行validation,而不是在我设置类型后实例化子类。

我知道这是预期的行为,因为我正在实例化一个不需要数量字段的类,但我想知道是否还有告诉rails运行子类的validation而不是super。

您可以使用自定义validation器解决此问题,类似于此问题的答案: 两个模型,一个STI和一个validation但是,如果您可以简单地实例化预期的子类型,则可以避免需要自定义在这种情况下完全是validation器。

正如您所注意到的那样,单独设置类型字段并不会将实例从一种类型神奇地更改为另一种类型。 虽然ActiveRecord将在从数据库中读取对象时使用type字段来实例化正确的类,但是反过来(实例化超类,然后手动更改类型字段)不具有更改对象类型的效果你的应用程序正在运行 – 它不会那样工作。

另一方面,自定义validation方法可以独立检查type字段,实例化相应类型的副本(基于type字段的值),然后运行.valid? 在该对象上,导致子类的validation以看似动态的方式运行,即使它实际上是在该过程中创建适当子类的实例。

我做了类似的事情。

使其适应您的问题:

 class Budget < ActiveRecord::Base validates_presence_of :price validates_presence_of :quantity, if: :hourly_rate? def hourly_rate? self.class.name == 'HourlyRateBudget' end end 

对于任何寻找示例代码的人来说,这是我实现第一个答案的方式:

 validate :subclass_validations def subclass_validations # Typecast into subclass to check those validations if self.class.descends_from_active_record? subclass = self.becomes(self.type.classify.constantize) self.errors.add(:base, "subclass validations are failing.") unless subclass.valid? end end 

而不是设置类型直接设置类似的类型…而是,尝试:

 new_type = params.fetch(:type) class_type = case new_type when "HourlyRateBudget" HourlyRateBudget when "FlatRateBudget" FlatRateBudget else raise StandardError.new "unknown budget type: #{new_type}" end class_type.new(:price => 10) 

您甚至可以通过以下方式将字符串转换为其类: new_type.classify.constantize但是如果它是从params进来的,那似乎有点危险。

如果你这样做,那么你将得到一个HourlyRateBudget类,否则它只是预算。

更好的是,使用type.constantize.new("10") ,但是这取决于params的类型必须是与HourlyRateBudget.class.to_s相同的正确字符串

我也需要同样的,在Bryce的帮助下我做了这个:

 class ActiveRecord::Base validate :subclass_validations, :if => Proc.new{ is_sti_supported_table? } def is_sti_supported_table? self.class.columns_hash.include? (self.class.inheritance_column) end def subclass_validations subclass = self.class.send(:compute_type, self.type) unless subclass == self.class subclass_obj= self.becomes(subclass) self.errors.add(:base, subclass_obj.errors.full_messages.join(', ')) unless subclass_obj.valid? end end end 

按照@franzlorenzon的回答,但是使用duck typing来避免在超类中引用类类型:

 class Budget < ActiveRecord::Base validates_presence_of :price validates_presence_of :quantity, if: :hourly_rate? def hourly_rate? false end end class HourlyRateBudget < Budget def hourly_rate? true end end