控制Rails验证的顺序

54

我有一个Rails模型,用户通过表单填写了其中的7个数字属性。

我需要验证每个属性是否存在,这显然很容易使用以下方法:

validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes

然而我还需要运行一个自定义验证器,它需要获取一些属性并对它们进行计算。如果这些计算的结果不在某个范围内,则应将模型声明为无效。

单独考虑这个问题也很容易。

validate :calculations_ok?

def calculations_ok?
  errors[:base] << "Not within required range" unless within_required_range?
end

def within_required_range?
  # check the calculations and return true or false here
end

问题在于“验证”方法总是在“校验”方法之前运行。这意味着,如果用户将所需字段中的任何一个留空,Rails 在尝试使用空白属性进行计算时会抛出错误。

那么,我该如何首先检查所有必需属性是否存在?


17
如果我没记错的话,Rails 总是运行所有验证,即使第一个验证无效。因此,即使您可以更改验证的顺序,如果实际上尝试执行无效操作,计算仍会导致错误。最好在执行计算之前手动检查它是否为空白。 - DanneManne
所以只需捕获错误并像以前一样添加到errors[:base]中。 - noodl
如果我只是捕获错误,我认为我无法向用户传达任何关于导致问题的信息(例如,他们漏掉了哪个字段)? - David Tuite
1
@DanneManne 我觉得这可能是真的。我希望有一种方法能够在中途停止验证并呈现错误信息页面。 - David Tuite
5个回答

22

我不确定这些验证会以什么顺序运行,因为它可能取决于attributes哈希本身的排序方式。如果某些必需的数据丢失,您最好使您的validate方法更具弹性并且不运行。例如:

def within_required_range?
  return if ([ a, b, c, d ].any?(&:blank?))

  # ...
end

如果变量ad中有任何一个为空(包括nil、空数组或字符串等),那么程序将退出。


12
虽然看起来不太像Rails的风格。用这种方式做意味着我不仅需要检查每个属性是否存在,还要检查其他一些东西,比如数字格式。这基本上意味着我要进行很多次验证。 - David Tuite
2
我再试了一下,看起来这确实是我需要做的。我认为DanneManne说的是对的,即使早期的验证失败,Rails也会运行所有验证。 - David Tuite
当使用return时,自定义验证方法会失败吗?Rails指南提到errors.add是触发验证失败的唯一方式。你能给我指点一些阅读材料吗? - Heisenberg
@Heisenberg 是的,你需要使用 errors.add 来在 return 之后包含任何消息。这只是表达逻辑以避免触发错误。 - tadman
我知道我来晚了,但这里的 find 应该被替换为 any? 吗?find 返回元素,而在这种情况下可能是 nil - cozyconemotel
1
@cozyconemotel 可能是更好的方法,说得好。 - tadman

9

在稍微复杂的情况下,一种替代方法是创建一个辅助方法,先运行依赖属性的验证。然后您可以有条件地运行:calculations_ok? 验证。

validates :attribute1, :presence => true
validates :attribute2, :presence => true
...
validates :attribute7, :presence => true

validate :calculations_ok?, :unless => Proc.new { |a| a.dependent_attributes_valid? }

def dependent_attributes_valid?
  [:attribute1, ..., :attribute7].each do |field|
    self.class.validators_on(field).each { |v| v.validate(self) }
    return false if self.errors.messages[field].present?
  end
  return true
end

我必须为一个项目创建类似这样的东西,因为依赖属性的验证非常复杂。我的“calculations_ok?”等效物会在依赖属性未正确验证时抛出异常。
优点:
- 相对DRY,特别是如果您的验证很复杂 - 确保您的错误数组报告正确的失败验证而不是宏验证 - 自动包括您稍后添加的依赖属性上的任何其他验证
注意事项:
- 可能会运行所有验证两次 - 您可能不希望在依赖属性上运行所有验证

1
虽然对于某些应用程序来说可能有点过度,但我不明白为什么没有更多的人喜欢这个解决方案。我发现它在类似复杂情境下非常棒! - ludwigschubert
@james-h 如果我有嵌套属性并且它们有自己的验证,这样会起作用吗? - androidharry
@androidharry 如果没有进行彻底的单元测试,我不会相信它。关联验证通常存在漏洞。 - James H

2

虽然在一个单一的验证调用中,验证将按照它们列出的顺序进行,但是即使attribute2为空,Rails仍将继续检查calculations_ok?所以我认为我仍然会有问题? - David Tuite
1
是的,但您的示例显示您首先正在验证它们的存在,因此不应该有问题:验证所有必需属性的存在,然后使用calculations_ok进行验证:validates:attribute1,:presence => true; validates:attribute2,:presence => true validates:attribute1,:calculations_ok => true - David Sulc
不幸的是,对于检查超过一个属性的自定义验证器,使用ActiveModel::EachValidator无效。例如,验证器验证起始和结束时间戳的正确顺序,因此必须比较这两个时间戳。最好对于这种特殊情况放弃Rails的存在验证器,并将逻辑编写到自定义验证器中。 - John Whitley

1
James H 的解决方案对我来说是最有意义的。但需要考虑的一件事是,如果您在依赖验证条件上有条件,则还需要检查它们,以使 dependent_attributes_valid? 调用起作用。

例如:

    validates :attribute1, presence: true
    validates :attribute1, uniqueness: true, if: :attribute1?
    validates :attribute1, numericality: true, unless: Proc.new {|r| r.attribute1.index("@") }
    validates :attribute2, presence: true
    ...
    validates :attribute7, presence: true

    validate :calculations_ok?, unless: Proc.new { |a| a.dependent_attributes_valid? }

    def dependent_attributes_valid?
      [:attribute1, ..., :attribute7].each do |field|
        self.class.validators_on(field).each do |v|
          # Surely there is a better way with rails?
          existing_error = v.attributes.select{|a| self.errors[a].present? }.present?

          if_condition = v.options[:if]
          validation_if_condition_passes = if_condition.blank?
          validation_if_condition_passes ||= if_condition.class == Proc ? if_condition.call(self) : !!self.send(if_condition)

          unless_condition = v.options[:unless]
          validation_unless_condition_passes = unless_condition.blank?
          validation_unless_condition_passes ||= unless_condition.class == Proc ? unless_condition.call(self) : !!self.send(unless_condition)

          if !existing_error and validation_if_condition_passes and validation_unless_condition_passes
            v.validate(self)
          end
        end
        return false if self.errors.messages[field].present?
      end
      return true
    end

1
我记得很久以前遇到过这个问题,仍然不清楚验证顺序是否可以设置并且如果验证返回错误是否可以停止执行链。
我不认为Rails提供此选项。这是有道理的; 我们想显示记录上的所有错误(包括由于无效输入而导致失败后出现的错误)。
一种可能的方法是仅在要验证的输入存在时进行验证:
def within_required_range?
  return unless [attribute1, attribute2, ..].all?(&:present?)
  
  # check the calculations and return true or false here
end

使用Rails习惯的验证选项,使其更加美观和结构化(单一职责)。
validates :attribute1, :presence => true
validates :attribute2, :presence => true
# and so on through the attributes

validate :calculations_ok?, if: :attributes_present?

private
  def attributes_present?
    [attribute1, attribute2, ..].all?(&:present?)
  end

  def calculations_ok?
    errors[:base] << "Not within required range" unless within_required_range?
  end

  def within_required_range?
    # check the calculations and return true or false here
  end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接