如何在单表继承中运行子类的验证?

时间:2012-02-10 14:46:21

标签: ruby-on-rails validation activerecord single-table-inheritance

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

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

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在超类中运行验证,而不是在设置类型后实例化子类。

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

7 个答案:

答案 0 :(得分:9)

您可以使用自定义验证器解决此问题,类似于此问题的答案:Two models, one STI and a Validation但是,如果您可以简单地实例化预期的子类型,则可以避免需要自定义在这种情况下完全是验证器。

正如您所注意到的,单独设置类型字段并不会将实例从一种类型神奇地更改为另一种类型。虽然ActiveRecord将使用type字段在数据库中读取对象时实例化正确的类,反之亦然(实例化超类,然后手动更改类型字段)在应用程序运行时不会改变对象的类型 - 它只是不起作用。

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

答案 1 :(得分: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

答案 2 :(得分: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

答案 3 :(得分: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类,否则它只是预算。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

我也要求同样的,在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

答案 6 :(得分:0)

按照@ 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