12

在我的应用程序中,我有一个名为Budget的类。预算可以有多种类型。例如,假设有两个预算:FlatRateBudget和HourlyRateBudget。两者都从类Budget中继承。如何在单表继承中运行子类的验证?

这是我得到迄今:

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为子类而不是超级运行验证。

+0

当使用STI,我会避免实例从对象超类,并且只能与基类一起工作,我相信Rails将使用超类和子类独有的适当验证。 – firecape 2015-05-27 04:29:24

回答

9

你可以用自定义验证器来解决这个问题,类似于这个问题的答案:Two models, one STI and a Validation但是,如果你可以简单地实例化预定的子类型,那么你就可以避免需要一个自定义验证器这个案例。

正如您已经注意到的那样,单独设置类型字段并不会奇怪地将实例从一种类型更改为另一种类型。虽然ActiveRecord将使用type字段来实例化适当的类,当从数据库读取对象时,以相反方式(实例化超类,然后手动更改类型字段)不会改变对象的效果在你的应用程序运行时键入 - 它不会以这种方式工作。

定制验证方法,在另一方面,可独立地检查type字段,实例化适当的类型(基于type字段的值)的一个副本,然后在该对象上运行.valid?,导致即使在流程中实际创建了适当子类的实例,子类的验证仍然是动态的。

+0

我用你的最后一段提示,它的工作。 – robertokl 2012-02-10 17:09:42

2

而不是设置类型的直接设置这样的类型......相反,尝试:

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,否则它只是预算。

+0

这是一种方法,但我想使用accept_nested_attributes魔术来保持我的控制器尽可能薄。 – robertokl 2012-02-10 16:44:27

+0

您绝对可以仍然这样做。诀窍是让你的字符串类型到一个类。 – 2012-02-10 17:53:33

3

为寻找示例代码,这里就是我实现了第一个答案:

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 
+0

它对我产生这个错误:NoMethodError(未定义的方法'类型'为 – Antzi 2013-04-22 14:39:36

+1

您的模型有一个“类型”字段吗? – Bryce 2013-04-22 17:25:27

0

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

+0

这个答案是我要推荐的,但是你想要白名单什么是可以接受的,所以有人不会'T传递一个恶意'type' 在'before_action'你可以这样做: '呈现状态:禁止,除非type.constantize.in?([HourlyRateBudget,FlatRateBudget])' – mackshkatz 2016-12-09 01:19:49

6

我做了类似的事情。

它适应您的问题:

class Budget < ActiveRecord::Base 

    validates_presence_of :price 
    validates_presence_of :quantity, if: :hourly_rate? 

    def hourly_rate? 
     self.class.name == 'HourlyRateBudget' 
    end 

end 
+0

这ISN'没有得到任何upvotes ...但它在我看来(作为一个铁轨noob)作为一个非常railvil解决方案。这是否工作,这仍然是你的建议解决这个问题的方式吗? – 2015-09-29 15:06:17

+0

很简单的答案!你可以使用自己.type =='HourlyRateBudget'' – hackhowtofaq 2015-12-03 10:20:29

0

我也需要相同的,并与布莱斯回答的帮助下,我这样做:

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