更多内容请访问 rubyonrails.org:

1 验证概述

这是一个非常简单的验证示例

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

如您所见,我们的验证让我们知道我们的 Person 在没有 name 属性的情况下是无效的。第二个 Person 不会被持久化到数据库中。

在深入了解更多细节之前,让我们谈谈验证如何融入应用程序的大局。

1.1 为什么要使用验证?

验证用于确保只有有效数据被保存到您的数据库中。例如,对于您的应用程序而言,确保每个用户都提供有效的电子邮件地址和邮寄地址可能很重要。模型级别的验证是确保只有有效数据被保存到您的数据库中的最佳方法。它们与数据库无关,无法被最终用户绕过,并且易于测试和维护。Rails 为常见需求提供了内置的助手,并允许您创建自己的验证方法。

在数据保存到您的数据库之前,还有几种其他方法可以验证数据,包括本机数据库约束、客户端验证和控制器级别的验证。以下是优缺点的总结

  • 数据库约束和/或存储过程使验证机制依赖于数据库,并且可能使测试和维护变得更加困难。但是,如果您的数据库被其他应用程序使用,那么最好在数据库级别使用一些约束。此外,数据库级别的验证可以安全地处理一些难以用其他方式实现的事情(例如,在使用频繁的表中确保唯一性)。
  • 客户端验证很有用,但如果单独使用,通常不可靠。如果使用 JavaScript 实现它们,则如果用户浏览器中禁用了 JavaScript,则可能会绕过它们。但是,如果与其他技术结合使用,客户端验证可以成为一种方便的方式,以便在用户使用您的网站时向他们提供即时反馈。
  • 控制器级别的验证可能很诱人,但通常会变得笨拙,难以测试和维护。只要有可能,最好保持控制器的简洁,这样会让您在长期内享受使用应用程序的乐趣。

在某些特定情况下选择这些方法。Rails 团队认为,在大多数情况下,模型级别的验证是最合适的。

1.2 何时发生验证?

Active Record 对象有两种:与数据库中的一行相对应的对象和与数据库中的一行不相对应的对象。当您创建一个新的对象时,例如使用 new 方法,该对象还不属于数据库。一旦您对该对象调用 save,它将被保存到相应的数据库表中。Active Record 使用 new_record? 实例方法来确定一个对象是否已在数据库中或尚未在数据库中。考虑以下 Active Record 类

class Person < ApplicationRecord
end

我们可以通过查看一些 bin/rails console 输出来看看它是如何工作的

irb> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>

irb> p.new_record?
=> true

irb> p.save
=> true

irb> p.new_record?
=> false

创建和保存新记录将向数据库发送 SQL INSERT 操作。更新现有记录将改为发送 SQL UPDATE 操作。验证通常在这些命令发送到数据库之前运行。如果任何验证失败,对象将被标记为无效,并且 Active Record 不会执行 INSERTUPDATE 操作。这避免了将无效对象存储在数据库中。您可以选择在创建、保存或更新对象时运行特定验证。

有许多方法可以更改数据库中对象的狀態。有些方法会触发验证,而有些方法则不会。这意味着如果您不小心,则有可能将对象以无效状态保存到数据库中。

以下方法触发验证,并且只有在对象有效的情况下才会将对象保存到数据库

带感叹号的版本(例如 save!)如果记录无效,则会引发异常。不带感叹号的版本不会:saveupdate 返回 false,而 create 返回对象。

1.3 跳过验证

以下方法跳过验证,并将无论对象是否有效都将其保存到数据库。应谨慎使用它们。请参考方法文档以了解更多信息。

请注意,save 还具有在传递 validate: false 作为参数时跳过验证的能力。应谨慎使用此技术。

  • save(validate: false)

1.4 valid?invalid?

在保存 Active Record 对象之前,Rails 会运行您的验证。如果这些验证产生任何错误,Rails 不会保存该对象。

您也可以自行运行这些验证。valid? 触发您的验证,如果对象中未发现任何错误,则返回 true,否则返回 false。如您在上面看到的

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

在 Active Record 完成验证后,可以通过 errors 实例方法访问任何失败,该方法返回一个错误集合。根据定义,如果在运行验证后此集合为空,则该对象有效。

请注意,使用 new 实例化的对象即使在技术上无效时也不会报告错误,因为验证仅在保存对象时(例如使用 createsave 方法)才会自动运行。

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> p = Person.new
=> #<Person id: nil, name: nil>
irb> p.errors.size
=> 0

irb> p.valid?
=> false
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p = Person.create
=> #<Person id: nil, name: nil>
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p.save
=> false

irb> p.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

irb> Person.create!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

invalid?valid? 的反面。它触发您的验证,如果对象中发现任何错误,则返回 true,否则返回 false。

1.5 errors[]

要验证对象的特定属性是否有效,您可以使用 errors[:attribute]。它返回 :attribute 的所有错误消息的数组。如果指定属性没有错误,则返回一个空数组。

此方法只有在运行验证之后才有用,因为它只检查错误集合,本身不会触发验证。它不同于上面解释的 ActiveRecord::Base#invalid? 方法,因为它不会验证整个对象的有效性。它只检查对象单个属性上是否有发现的错误。

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.new.errors[:name].any?
=> false
irb> Person.create.errors[:name].any?
=> true

我们将在处理验证错误部分更深入地介绍验证错误。

2 验证助手

Active Record 提供了许多预定义的验证助手,您可以直接在类定义中使用它们。这些助手提供了常见的验证规则。每次验证失败时,都会将错误添加到对象的 errors 集合中,并且与正在验证的属性相关联。

每个助手都接受任意数量的属性名称,因此只需一行代码,您就可以为多个属性添加相同类型的验证。

所有助手都接受 :on:message 选项,分别定义应何时运行验证以及如果验证失败应向 errors 集合添加什么消息。:on 选项接受 :create:update 之一的值。每个验证助手都有一个默认错误消息。当未指定 :message 选项时,将使用这些消息。让我们看一下每个可用的助手。

要查看可用的默认助手的列表,请查看ActiveModel::Validations::HelperMethods

2.1 acceptance

此方法验证在提交表单时是否选中了用户界面上的复选框。这通常用于用户需要同意应用程序的服务条款、确认已阅读某些文本或任何类似的概念。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

仅当 terms_of_service 不为 nil 时才会执行此检查。此助手的默认错误消息为“必须接受”。您还可以通过 message 选项传入自定义消息。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { message: "must be abided" }
end

它还可以接收 :accept 选项,该选项确定将被视为可接受的允许值。它默认为 ['1', true],可以轻松更改。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { accept: "yes" }
  validates :eula, acceptance: { accept: ["TRUE", "accepted"] }
end

此验证对 Web 应用程序非常特定,并且此“接受”不需要在您的数据库中的任何地方进行记录。如果您没有该字段,助手将创建一个虚拟属性。如果您的数据库中存在该字段,则必须将 accept 选项设置为或包括 true,否则验证将不会运行。

2.2 confirmation

当您有两个文本字段应该接收完全相同的内容时,您应该使用此助手。例如,您可能需要确认电子邮件地址或密码。此验证将创建一个虚拟属性,其名称是必须与之确认的字段的名称,并附加了“_confirmation”。

class Person < ApplicationRecord
  validates :email, confirmation: true
end

在您的视图模板中,您可以使用类似以下的内容

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

仅当 email_confirmation 不为 nil 时才会执行此检查。要要求确认,请确保为确认属性添加存在性检查(我们将在本指南中稍后了解 presence later)。

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true
end

还有一个 :case_sensitive 选项,您可以使用它来定义确认约束是区分大小写还是不区分大小写。此选项默认为 true。

class Person < ApplicationRecord
  validates :email, confirmation: { case_sensitive: false }
end

此助手的默认错误消息为“与确认不匹配”。您还可以通过 message 选项传入自定义消息。

通常,在使用此验证器时,您希望将其与 :if 选项结合使用,以便仅在初始字段发生更改时(而不是每次保存记录时)才验证“_confirmation”字段。更多关于 条件验证 稍后会介绍。

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true, if: :email_changed?
end

2.3 comparison

此检查将验证任何两个可比较值之间的比较。

class Promotion < ApplicationRecord
  validates :end_date, comparison: { greater_than: :start_date }
end

此助手的默认错误消息为“比较失败”。您还可以通过 message 选项传入自定义消息。

所有这些选项都受支持

  • :greater_than - 指定值必须大于提供的值。此选项的默认错误消息为“必须大于 %{count}”。
  • :greater_than_or_equal_to - 指定值必须大于或等于提供的值。此选项的默认错误消息为“必须大于或等于 %{count}”。
  • :equal_to - 指定值必须等于提供的值。此选项的默认错误消息为“必须等于 %{count}”。
  • :less_than - 指定值必须小于提供的值。此选项的默认错误消息为“必须小于 %{count}”。
  • :less_than_or_equal_to - 指定值必须小于或等于提供的值。此选项的默认错误消息为“必须小于或等于 %{count}”。
  • :other_than - 指定值必须与提供的值不同。此选项的默认错误消息为“必须与 %{count} 不同”。

验证器要求提供比较选项。每个选项都接受一个值、proc 或符号。任何包含 Comparable 的类都可以进行比较。

2.4 format

此助手通过测试属性值是否与给定的正则表达式匹配来验证属性值,正则表达式使用 :with 选项指定。

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "only allows letters" }
end

相反,通过使用 :without 选项代替,您可以要求指定的属性匹配正则表达式。

在这两种情况下,提供的 :with:without 选项必须是正则表达式或返回正则表达式的 proc 或 lambda。

默认错误消息为“无效”。

使用 \A\z 来匹配字符串的开头和结尾,^$ 匹配行的开头/结尾。由于 ^$ 的频繁误用,如果您在提供的正则表达式中使用这两个锚点,则需要传递 multiline: true 选项。在大多数情况下,您应该使用 \A\z

2.5 inclusion

此助手验证属性值是否包含在给定的集合中。实际上,此集合可以是任何可枚举对象。

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }
end

inclusion 助手有一个选项 :in,它接收将被接受的值集。:in 选项有一个别名为 :within,如果您愿意,可以使用它来实现相同的目的。前面的示例使用 :message 选项来显示如何包含属性的值。有关完整选项,请参见 消息文档

此助手的默认错误消息为“未包含在列表中”。

2.6 exclusion

inclusion 的反面是... exclusion

此助手验证属性值是否不包含在给定的集合中。实际上,此集合可以是任何可枚举对象。

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: %w(www us ca jp),
    message: "%{value} is reserved." }
end

exclusion 助手有一个选项 :in,它接收将不被接受的值集,用于验证的属性。:in 选项有一个别名为 :within,如果您愿意,可以使用它来实现相同的目的。此示例使用 :message 选项来显示如何包含属性的值。有关消息参数的完整选项,请参见 消息文档

默认错误消息为“已保留”。

除了传统的可枚举对象(如数组)之外,您还可以提供一个 proc、lambda 或符号,它返回一个可枚举对象。如果可枚举对象是数值、时间或日期时间范围,则使用 Range#cover? 执行测试,否则使用 include?。在使用 proc 或 lambda 时,将正在验证的实例作为参数传递。

2.7 length

此助手验证属性值的长度。它提供各种选项,因此您可以用不同的方式指定长度约束

class Person < ApplicationRecord
  validates :name, length: { minimum: 2 }
  validates :bio, length: { maximum: 500 }
  validates :password, length: { in: 6..20 }
  validates :registration_number, length: { is: 6 }
end

可能的长度约束选项是

  • :minimum - 属性的长度不能小于指定长度。
  • :maximum - 属性的长度不能大于指定长度。
  • :in(或 :within) - 属性长度必须包含在给定的间隔内。此选项的值必须是范围。
  • :is - 属性长度必须等于给定的值。

默认错误消息取决于正在执行的长度验证类型。您可以使用 :wrong_length:too_long:too_short 选项以及 %{count} 作为占位符来自定义这些消息,以表示与正在使用的长度约束相对应的数字。您仍然可以使用 :message 选项来指定错误消息。

class Person < ApplicationRecord
  validates :bio, length: { maximum: 1000,
    too_long: "%{count} characters is the maximum allowed" }
end

请注意,默认错误消息是复数形式(例如,“太短了(最短为 %{count} 个字符)”。因此,当 :minimum 为 1 时,您应该提供自定义消息或改为使用 presence: true。当 :in:within 的下限为 1 时,您应该提供自定义消息或在 length 之前调用 presence

除了 :minimum:maximum 选项之外,一次只能使用一个约束选项,它们可以组合在一起。

2.8 numericality

此助手验证您的属性是否只有数值。默认情况下,它将匹配一个可选的符号,后面跟着一个整数或浮点数。

要指定只允许整数,请将 :only_integer 设置为 true。然后它将使用以下正则表达式来验证属性的值。

/\A[+-]?\d+\z/

否则,它将尝试使用 Float 将值转换为数字。Float 将使用列的精度值或最多 15 位数字转换为 BigDecimal

class Player < ApplicationRecord
  validates :points, numericality: true
  validates :games_played, numericality: { only_integer: true }
end

:only_integer 的默认错误消息为“必须是整数”。

除了 :only_integer 之外,此助手还接受 :only_numeric 选项,该选项指定值必须是 Numeric 的实例,如果它是 String,则尝试解析该值。

默认情况下,numericality 不允许 nil 值。您可以使用 allow_nil: true 选项来允许它。请注意,对于 IntegerFloat 列,空字符串将转换为 nil

当没有指定选项时,默认错误消息为“不是数字”。

还有一些选项可用于添加对可接受值的约束

  • :greater_than - 指定值必须大于提供的值。此选项的默认错误消息为“必须大于 %{count}”。
  • :greater_than_or_equal_to - 指定值必须大于或等于提供的值。此选项的默认错误消息为“必须大于或等于 %{count}”。
  • :equal_to - 指定值必须等于提供的值。此选项的默认错误消息为“必须等于 %{count}”。
  • :less_than - 指定值必须小于提供的值。此选项的默认错误消息为“必须小于 %{count}”。
  • :less_than_or_equal_to - 指定值必须小于或等于提供的值。此选项的默认错误消息为“必须小于或等于 %{count}”。
  • :other_than - 指定值必须与提供的值不同。此选项的默认错误消息为“必须与 %{count} 不同”。
  • :in - 指定值必须在提供的范围内。此选项的默认错误消息为“必须在 %{count} 中”。
  • :odd - 指定值必须是奇数。此选项的默认错误消息为“必须是奇数”。
  • :even - 指定值必须是偶数。此选项的默认错误消息为“必须是偶数”。

2.9 presence

此助手验证指定的属性是否为空。它使用 Object#blank? 方法来检查值是否为 nil 或空字符串,即一个空字符串或仅包含空白字符的字符串。

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

如果要确保关联存在,则需要测试关联对象本身是否存在,而不是用于映射关联的外键。这样,不仅会检查外键是否为空,还会检查引用对象是否存在。

class Supplier < ApplicationRecord
  has_one :account
  validates :account, presence: true
end

为了验证需要存在的关联记录,必须为关联指定 :inverse_of 选项。

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

如果要确保关联既存在又有效,还需要使用 validates_associated。更多信息见 下文

如果验证通过 has_onehas_many 关系关联的对象的存在性,它将检查该对象是否为 blank?marked_for_destruction?

由于 false.blank? 为 true,如果要验证布尔字段的存在性,则应使用以下验证之一

# Value _must_ be true or false
validates :boolean_field_name, inclusion: [true, false]
# Value _must not_ be nil, aka true or false
validates :boolean_field_name, exclusion: [nil]

通过使用这些验证之一,您将确保该值不会是 nil,这在大多数情况下会导致 NULL 值。

默认错误消息为 "can't be blank"

2.10 absence

此助手验证指定的属性是否不存在。它使用 Object#present? 方法来检查值是否既不是 nil 也不是空字符串,即一个空字符串或仅包含空白字符的字符串。

class Person < ApplicationRecord
  validates :name, :login, :email, absence: true
end

如果要确保关联不存在,则需要测试关联对象本身是否不存在,而不是用于映射关联的外键。

class LineItem < ApplicationRecord
  belongs_to :order
  validates :order, absence: true
end

为了验证需要不存在的关联记录,必须为关联指定 :inverse_of 选项。

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

如果要确保关联既存在又有效,还需要使用 validates_associated。更多信息见 下文

如果验证通过 has_onehas_many 关系关联的对象的不存在性,它将检查该对象是否为 present?marked_for_destruction?

由于 false.present? 为 false,如果要验证布尔字段的不存在性,则应使用 validates :field_name, exclusion: { in: [true, false] }

默认错误消息为 "must be blank"

2.11 uniqueness

此助手验证属性的值在对象保存之前是否唯一。

class Account < ApplicationRecord
  validates :email, uniqueness: true
end

验证通过在模型的表中执行 SQL 查询来完成,查找在该属性中具有相同值的现有记录。

有一个 :scope 选项,可用于指定一个或多个属性,这些属性用于限制唯一性检查。

class Holiday < ApplicationRecord
  validates :name, uniqueness: { scope: :year,
    message: "should happen once per year" }
end

此验证不会在数据库中创建唯一性约束,因此可能发生两个不同的数据库连接创建了两条记录,它们具有相同的值,而您希望该列是唯一的。为了避免这种情况,您必须在数据库中该列上创建唯一索引。

为了在数据库中添加唯一性数据库约束,请在迁移中使用 add_index 语句,并包含 unique: true 选项。

如果您希望创建数据库约束以防止使用 :scope 选项可能违反唯一性验证,则必须在数据库中的两列上创建唯一索引。有关多列索引的更多详细信息,请参阅 MySQL 手册MariaDB 手册,或者 PostgreSQL 手册 中的唯一约束示例,这些约束引用一组列。

还有一个 :case_sensitive 选项,可用于定义唯一性约束是区分大小写的,不区分大小写的,还是应遵循默认数据库排序规则。此选项默认为遵循默认数据库排序规则。

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

请注意,某些数据库被配置为始终执行不区分大小写的搜索。

有一个 :conditions 选项,您可以在其中指定其他条件作为 WHERE SQL 片段以限制唯一性约束查找(例如 conditions: -> { where(status: 'active') })。

默认错误消息为 "has already been taken"

有关更多信息,请参阅 validates_uniqueness_of

2.12 validates_associated

当您的模型具有始终需要验证的关联时,应使用此助手。每次尝试保存对象时,都会对每个关联对象调用 valid?

class Library < ApplicationRecord
  has_many :books
  validates_associated :books
end

此验证适用于所有关联类型。

不要在关联的两端都使用 validates_associated。它们会相互调用,形成一个无限循环。

默认错误消息为 validates_associated"is invalid"。请注意,每个关联对象都将包含自己的 errors 集合;错误不会冒泡到调用模型。

validates_associated 只能与 ActiveRecord 对象一起使用,到目前为止,所有内容也可以在包含 ActiveModel::Validations 的任何对象上使用。

2.13 validates_each

此助手使用代码块验证属性。它没有预定义的验证函数。您应使用代码块创建一个验证函数,并将传递给 validates_each 的每个属性都将针对它进行测试。

在以下示例中,我们将拒绝以小写字母开头的名字和姓氏。

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, "must start with upper case") if /\A[[:lower:]]/.match?(value)
  end
end

代码块接收记录、属性的名称和属性的值。

您可以在代码块中执行任何操作来检查有效数据。如果验证失败,则应向模型添加错误,从而使其无效。

2.14 validates_with

此助手将记录传递给单独的类进行验证。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if record.first_name == "Evil"
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator
end

validates_with 没有默认错误消息。您必须手动将错误添加到记录的错误集合中,在验证器类中。

添加到 record.errors[:base] 的错误与记录整体的状态有关。

要实现 validate 方法,您必须在方法定义中接受一个 record 参数,它是要验证的记录。

如果要在特定属性上添加错误,请将其作为第一个参数传递,例如 record.errors.add(:first_name, "please choose another name")。我们将在后面详细介绍 验证错误

def validate(record)
  if record.some_field != "acceptable"
    record.errors.add :some_field, "this field is unacceptable"
  end
end

validates_with 助手采用一个类或要用于验证的类列表。

class Person < ApplicationRecord
  validates_with MyValidator, MyOtherValidator, on: :create
end

与所有其他验证一样,validates_with 接受 :if:unless:on 选项。如果您传递任何其他选项,它会将这些选项作为 options 传递给验证器类。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any? { |field| record.send(field) == "Evil" }
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

请注意,验证器将 只初始化一次,用于整个应用程序生命周期,而不是在每次验证运行时,因此请谨慎使用其中的实例变量。

如果您的验证器足够复杂,以至于您想要使用实例变量,则可以轻松地使用普通的 Ruby 对象代替

class Person < ApplicationRecord
  validate do |person|
    GoodnessValidator.new(person).validate
  end
end

class GoodnessValidator
  def initialize(person)
    @person = person
  end

  def validate
    if some_complex_condition_involving_ivars_and_private_methods?
      @person.errors.add :base, "This person is evil"
    end
  end

  # ...
end

我们将在后面详细介绍 自定义验证

3 常见的验证选项

我们刚刚介绍的验证器支持多个常见选项,现在让我们介绍其中的一些!

并非所有这些选项都受每个验证器支持,请参阅 ActiveModel::Validations 的 API 文档。

通过使用我们刚刚提到的任何验证方法,还有一些常见选项与验证器一起共享。我们现在将介绍这些选项!

  • :allow_nil:如果属性为 nil,则跳过验证。
  • :allow_blank:如果属性为空,则跳过验证。
  • :message:指定自定义错误消息。
  • :on:指定此验证处于活动状态的上下文。
  • :strict:在验证失败时引发异常。
  • :if:unless:指定验证何时应该或不应该发生。

3.1 :allow_nil

:allow_nil 选项在被验证的值为 nil 时跳过验证。

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }, allow_nil: true
end
irb> Coffee.create(size: nil).valid?
=> true
irb> Coffee.create(size: "mega").valid?
=> false

有关消息参数的完整选项,请参阅 消息文档

3.2 :allow_blank

:allow_blank 选项类似于 :allow_nil 选项。如果属性的值为 blank?,例如 nil 或空字符串,此选项将使验证通过。

class Topic < ApplicationRecord
  validates :title, length: { is: 5 }, allow_blank: true
end
irb> Topic.create(title: "").valid?
=> true
irb> Topic.create(title: nil).valid?
=> true

3.3 :message

如您所见,:message 选项允许您指定在验证失败时将添加到 errors 集合中的消息。当未使用此选项时,Active Record 将使用每个验证助手的默认错误消息。

:message 选项接受 StringProc 作为其值。

String :message 值可以选择包含任何/所有 %{value}%{attribute}%{model},这些值将在验证失败时动态替换。此替换是使用 i18n gem 完成的,占位符必须完全匹配,不允许空格。

class Person < ApplicationRecord
  # Hard-coded message
  validates :name, presence: { message: "must be given please" }

  # Message with dynamic attribute value. %{value} will be replaced
  # with the actual value of the attribute. %{attribute} and %{model}
  # are also available.
  validates :age, numericality: { message: "%{value} seems wrong" }
end

Proc :message 值将获得两个参数:要验证的对象和一个包含 :model:attribute:value 键值对的哈希表。

class Person < ApplicationRecord
  validates :username,
    uniqueness: {
      # object = person object being validated
      # data = { model: "Person", attribute: "Username", value: <username> }
      message: ->(object, data) do
        "Hey #{object.name}, #{data[:value]} is already taken."
      end
    }
end

3.4 :on

:on 选项允许您指定验证何时应该发生。所有内置验证助手的默认行为是在保存时运行(在创建新记录和更新记录时都运行)。如果您要更改它,可以使用 on: :create 仅在新记录创建时运行验证,或使用 on: :update 仅在记录更新时运行验证。

class Person < ApplicationRecord
  # it will be possible to update email with a duplicated value
  validates :email, uniqueness: true, on: :create

  # it will be possible to create the record with a non-numerical age
  validates :age, numericality: true, on: :update

  # the default (validates on both create and update)
  validates :name, presence: true
end

您也可以使用 `on:` 来定义自定义上下文。自定义上下文需要通过将上下文名称传递给 `valid?`、`invalid?` 或 `save` 来显式触发。

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
end
irb> person = Person.new(age: 'thirty-three')
irb> person.valid?
=> true
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"]}

person.valid?(:account_setup) 会执行所有验证,但不会保存模型。`person.save(context: :account_setup)` 会在保存之前在 `account_setup` 上下文中验证 `person`。

传递一个符号数组也是可以接受的。

class Book
  include ActiveModel::Validations

  validates :title, presence: true, on: [:update, :ensure_title]
end
irb> book = Book.new(title: nil)
irb> book.valid?
=> true
irb> book.valid?(:ensure_title)
=> false
irb> book.errors.messages
=> {:title=>["can't be blank"]}

当由显式上下文触发时,将针对该上下文以及任何没有上下文的验证运行验证。

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
  validates :name, presence: true
end
irb> person = Person.new
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]}

我们将在回调指南中介绍 `on:` 的更多用例。

4 严格验证

您也可以指定验证为严格的,并在对象无效时引发 `ActiveModel::StrictValidationFailed`。

class Person < ApplicationRecord
  validates :name, presence: { strict: true }
end
irb> Person.new.valid?
ActiveModel::StrictValidationFailed: Name can't be blank

您还可以将自定义异常传递给 `:strict` 选项。

class Person < ApplicationRecord
  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
irb> Person.new.valid?
TokenGenerationException: Token can't be blank

5 条件验证

有时,只有在满足给定谓词时才对对象进行验证才有意义。您可以使用 `:if` 和 `:unless` 选项来做到这一点,它们可以接受符号、`Proc` 或 `Array`。当您想要指定验证应该发生的时间时,您可以使用 `:if` 选项。或者,如果您想指定验证不应该发生的时间,则可以使用 `:unless` 选项。

5.1 使用 `:if` 和 `:unless` 的符号

您可以将 `:if` 和 `:unless` 选项与对应于将在验证发生之前调用的方法名称的符号相关联。这是最常用的选项。

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

5.2 使用 `:if` 和 `:unless` 的 Proc

可以将 `:if` 和 `:unless` 与将被调用的 `Proc` 对象相关联。使用 `Proc` 对象可以让您编写内联条件而不是单独的方法。此选项最适合单行代码。

class Account < ApplicationRecord
  validates :password, confirmation: true,
    unless: Proc.new { |a| a.password.blank? }
end

由于 `lambda` 是 `Proc` 的一种类型,因此它也可以用来编写内联条件,利用缩短的语法。

validates :password, confirmation: true, unless: -> { password.blank? }

5.3 对条件验证进行分组

有时,让多个验证使用一个条件是有用的。可以使用 with_options 轻松实现。

class User < ApplicationRecord
  with_options if: :is_admin? do |admin|
    admin.validates :password, length: { minimum: 10 }
    admin.validates :email, presence: true
  end
end

with_options 块中的所有验证都会自动传递条件 `if: :is_admin?`

5.4 组合验证条件

另一方面,当多个条件定义验证是否应该发生时,可以使用 `Array`。此外,您可以在同一个验证中应用 `:if` 和 `:unless`。

class Computer < ApplicationRecord
  validates :mouse, presence: true,
                    if: [Proc.new { |c| c.market.retail? }, :desktop?],
                    unless: Proc.new { |c| c.trackpad.present? }
end

仅当所有 `:if` 条件都被评估为 `true`,而所有 `:unless` 条件都被评估为 `false` 时,才会运行验证。

6 执行自定义验证

当内置的验证助手不足以满足您的需求时,您可以根据需要编写自己的验证器或验证方法。

6.1 自定义验证器

自定义验证器是从 ActiveModel::Validator 继承的类。这些类必须实现 `validate` 方法,该方法以记录为参数,并对其执行验证。自定义验证器使用 `validates_with` 方法调用。

class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.start_with? "X"
      record.errors.add :name, "Provide a name starting with X, please!"
    end
  end
end

class Person < ApplicationRecord
  validates_with MyValidator
end

使用方便的 ActiveModel::EachValidator,为验证单个属性添加自定义验证器的最简单方法是。在这种情况下,自定义验证器类必须实现一个 `validate_each` 方法,该方法接受三个参数:记录、属性和值。它们对应于实例、要验证的属性以及传递的实例中属性的值。

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      record.errors.add attribute, (options[:message] || "is not an email")
    end
  end
end

class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

如示例所示,您还可以将标准验证与您自己的自定义验证器相结合。

6.2 自定义方法

您还可以创建方法来验证模型的状态,并在它们无效时将错误添加到 `errors` 集合中。然后,您必须使用 validate 类方法注册这些方法,传入验证方法名称的符号。

您可以为每个类方法传递多个符号,并且相应的验证将按注册顺序运行。

valid? 方法将验证 `errors` 集合是否为空,因此,当您希望验证失败时,您的自定义验证方法应该将错误添加到其中。

class Invoice < ApplicationRecord
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value

  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end

  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end

默认情况下,这些验证将在您每次调用 `valid?` 或保存对象时运行。但是,还可以通过在 `validate` 方法中提供 `:on` 选项来控制何时运行这些自定义验证,可以使用 `:create` 或 `:update`。

class Invoice < ApplicationRecord
  validate :active_customer, on: :create

  def active_customer
    errors.add(:customer_id, "is not active") unless customer.active?
  end
end

有关 :on 的更多详细信息,请参阅上面的部分。

6.3 列出验证器

如果您想找出给定对象的全部验证器,请查看 `validators`。

例如,如果我们有以下使用自定义验证器和内置验证器的模型

class Person < ApplicationRecord
  validates :name, presence: true, on: :create
  validates :email, format: URI::MailTo::EMAIL_REGEXP
  validates_with MyOtherValidator, strict: true
end

现在,我们可以使用 "Person" 模型上的 `validators` 列出所有验证器,甚至使用 `validators_on` 检查特定字段。

irb> Person.validators
#=> [#<ActiveRecord::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={:on=>:create}>,
     #<MyOtherValidatorValidator:0x10b2f17d0
      @attributes=[:name], @options={:strict=>true}>,
     #<ActiveModel::Validations::FormatValidator:0x10b2f0f10
      @attributes=[:email],
      @options={:with=>URI::MailTo::EMAIL_REGEXP}>]
     #<MyOtherValidator:0x10b2f0948 @options={:strict=>true}>]

irb> Person.validators_on(:name)
#=> [#<ActiveModel::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={on: :create}>]

7 处理验证错误

valid?invalid? 方法仅提供有关有效性的摘要状态。但是,您可以使用 errors 集合中的各种方法更深入地了解每个错误。

以下是最常用的方法列表。有关所有可用方法的列表,请参阅 ActiveModel::Errors 文档。

7.1 errors

您可以通过它深入了解每个错误的各种详细信息。

这将返回一个包含所有错误的 `ActiveModel::Errors` 类实例,每个错误都由一个 ActiveModel::Error 对象表示。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.full_messages
=> ["Name can't be blank", "Name is too short (minimum is 3 characters)"]

irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors.full_messages
=> []

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.first.details
=> {:error=>:too_short, :count=>3}

7.2 errors[]

当您想检查特定属性的错误消息时,使用 errors[]。它返回一个包含给定属性的所有错误消息的字符串数组,每个字符串包含一条错误消息。如果没有与属性相关的错误,则返回一个空数组。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors[:name]
=> []

irb> person = Person.new(name: "JD")
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["is too short (minimum is 3 characters)"]

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["can't be blank", "is too short (minimum is 3 characters)"]

7.3 errors.where 和错误对象

有时,除了错误消息之外,我们可能还需要更多关于每个错误的信息。每个错误都被封装为一个 `ActiveModel::Error` 对象,而 where 方法是访问它的最常用方式。

where 返回一个通过各种条件过滤的错误对象数组。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end

我们可以通过将 `attribute` 作为第一个参数传递给 `errors.where(:attr)` 来仅过滤 `attribute`。第二个参数用于通过调用 `errors.where(:attr, :type)` 来过滤我们想要的错误 `type`。

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name)
=> [ ... ] # all errors for :name attribute

irb> person.errors.where(:name, :too_short)
=> [ ... ] # :too_short errors for :name attribute

最后,我们可以通过给定类型的错误对象上可能存在的任何 `options` 来进行过滤。

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name, :too_short, minimum: 3)
=> [ ... ] # all name errors being too short and minimum is 2

您可以从这些错误对象中读取各种信息

irb> error = person.errors.where(:name).last

irb> error.attribute
=> :name
irb> error.type
=> :too_short
irb> error.options[:count]
=> 3

您也可以生成错误消息

irb> error.message
=> "is too short (minimum is 3 characters)"
irb> error.full_message
=> "Name is too short (minimum is 3 characters)"

full_message 方法将生成更友好的消息,并在前面加上大写的属性名称。(要自定义 `full_message` 使用的格式,请参阅 I18n 指南。)

7.4 errors.add

add 方法通过接受 `attribute`、错误 `type` 和其他选项哈希来创建错误对象。当编写您自己的验证器时,这很有用,因为它让您可以定义非常具体的错误情况。

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_plain, message: "is not cool enough"
  end
end
irb> person = Person.create
irb> person.errors.where(:name).first.type
=> :too_plain
irb> person.errors.where(:name).first.full_message
=> "Name is not cool enough"

7.5 errors[:base]

您可以添加与对象状态整体相关而不是与特定属性相关的错误。为此,在添加新错误时,必须使用 `:base` 作为属性。

class Person < ApplicationRecord
  validate do |person|
    errors.add :base, :invalid, message: "This person is invalid because ..."
  end
end
irb> person = Person.create
irb> person.errors.where(:base).first.full_message
=> "This person is invalid because ..."

7.6 errors.size

size 方法返回对象的总错误数量。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.size
=> 2

irb> person = Person.new(name: "Andrea", email: "[email protected]")
irb> person.valid?
=> true
irb> person.errors.size
=> 0

7.7 errors.clear

clear 方法用于在您有意要清除 `errors` 集合时使用。当然,对无效对象调用 `errors.clear` 实际上不会使其有效:`errors` 集合现在为空,但下次您调用 `valid?` 或尝试将此对象保存到数据库中的任何方法时,验证将再次运行。如果任何验证失败,`errors` 集合将再次填充。

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.empty?
=> false

irb> person.errors.clear
irb> person.errors.empty?
=> true

irb> person.save
=> false

irb> person.errors.empty?
=> false

8 在视图中显示验证错误

创建模型并添加验证后,如果该模型是通过 Web 表单创建的,您可能希望在验证失败时显示错误消息。

由于每个应用程序处理此类事情的方式不同,因此 Rails 不包含任何视图助手来帮助您直接生成这些消息。但是,由于 Rails 提供了大量方法与验证进行交互,因此您可以构建自己的方法。此外,在生成脚手架时,Rails 会将一些 ERB 放入它生成的 `_form.html.erb` 中,该 ERB 会显示该模型上的完整错误列表。

假设我们有一个模型,它已保存在名为 `@article` 的实例变量中,它看起来像这样

<% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

    <ul>
      <% @article.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

此外,如果您使用 Rails 表单助手来生成表单,则当某个字段发生验证错误时,它会生成一个围绕该条目的额外 `<div>`。

<div class="field_with_errors">
  <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>

然后,您可以根据需要设置此 div 的样式。例如,Rails 生成的默认脚手架添加了以下 CSS 规则

.field_with_errors {
  padding: 2px;
  background-color: red;
  display: table;
}

这意味着任何带有错误的字段最终都会带有 2 像素的红色边框。



返回顶部