更多内容请访问 rubyonrails.org:

1. 验证概述

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

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

如您所见,如果没有 name 属性,则 Person 无效。

在我们深入探讨更多细节之前,让我们讨论一下验证如何适应您的应用程序的整体情况。

1.1. 为什么要使用验证?

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

1.2. 其他验证方式

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

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

Rails 建议在大多数情况下使用模型级验证,但是可能存在需要用其他验证来补充它们的特定情况。

1.3. 验证触发器

Active Record 对象有两种类型——一种对应于数据库中的一行,另一种不对应。当您使用 new 方法实例化一个新对象时,该对象尚未保存到数据库中。一旦您在该对象上调用 save,它将被保存到相应的数据库表中。Active Record 使用一个名为 persisted?(及其反向 new_record?)的实例方法来确定对象是否已存在于数据库中。考虑以下 Active Record 类

class Person < ApplicationRecord
end

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

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

irb> p.new_record?
=> true

irb> p.persisted?
=> false

irb> p.save
=> true

irb> p.new_record?
=> false

irb> p.persisted?
=> true

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

虽然验证通常可以防止无效数据保存到数据库中,但重要的是要意识到并非 Rails 中的所有方法都会触发验证。有些方法允许直接对数据库进行更改而不执行验证。因此,如果您不小心,可能会绕过验证并以无效状态保存对象。

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

感叹号版本(以感叹号结尾的方法,如 save!)如果记录无效则会引发异常。非感叹号版本——saveupdate 返回 false,而 create 返回对象。

1.4. 跳过验证

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

如果将 validate: false 作为参数传递,save 还可以跳过验证。此技术应谨慎使用。

1.5. 检查有效性

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

您也可以自行运行验证。valid? 触发您的验证,如果对象中未发现错误则返回 true,否则返回 false。如您上面所见

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

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

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

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

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

irb> person.save
=> false

irb> person.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.6. 检查和处理错误

要验证对象的特定属性是否有效,可以使用 errors[:attribute]。它返回一个包含 :attribute 所有错误消息的数组。如果指定属性上没有错误,则返回一个空数组。这使您可以轻松确定特定属性是否存在任何验证问题。

以下是一个说明如何检查属性错误的示例

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> new_person = Person.new
irb> new_person.errors[:name]
=> [] # no errors since validations are not run until saved
irb> new_person.errors[:name].any?
=> false

irb> create_person = Person.create
irb> create_person.errors[:name]
=> ["can't be blank"] # validation error because `name` is required
irb> create_person.errors[:name].any?
=> true

此外,您可以使用 errors.add 方法手动为特定属性添加错误消息。这在定义自定义验证场景时特别有用。

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_short, message: "is not long enough"
  end
end

要更深入地了解验证错误,请参阅处理验证错误部分。

2. 验证

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

当验证失败时,错误消息会存储在 errors 集合中,在触发验证的属性名称下。这意味着您可以轻松访问与任何特定属性相关的错误。例如,如果您验证 :name 属性并且验证失败,您将在 errors[:name] 下找到错误消息。

在现代 Rails 应用程序中,通常使用更简洁的验证语法,例如

validates :name, presence: true

然而,旧版本的 Rails 使用“helper”方法,例如

validates_presence_of :name

两种表示法执行相同的功能,但推荐使用较新的形式,因为它具有可读性并符合 Rails 的约定。

每个验证都接受任意数量的属性名称,允许您在单个代码行中将相同类型的验证应用于多个属性。

此外,所有验证都接受 :on:message 选项。:on 选项指定何时应触发验证,可能的值为 :create:update:message 选项允许您定义自定义错误消息,如果验证失败,该消息将添加到错误集合中。如果您不指定消息,Rails 将为该验证使用默认错误消息。

要查看可用默认助手的列表,请查看 ActiveModel::Validations::HelperMethods。此 API 部分使用上述旧表示法。

下面我们概述最常用的验证。

2.1. absence

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

#absence 通常用于条件验证。例如

class Person < ApplicationRecord
  validates :phone_number, :address, absence: true, if: :invited?
end
irb> person = Person.new(name: "Jane Doe", invitation_sent_at: Time.current)
irb> person.valid?
=> true # absence validation passes

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

class LineItem < ApplicationRecord
  belongs_to :order, optional: true
  validates :order, absence: true
end
irb> line_item = LineItem.new
irb> line_item.valid?
=> true # absence validation passes

order = Order.create
irb> line_item_with_order = LineItem.new(order: order)
irb> line_item_with_order.valid?
=> false # absence validation fails

对于 belongs_to,默认验证关联存在。如果您不想验证关联存在,请使用 optional: true

Rails 通常会自动推断反向关联。在您使用自定义 :foreign_key:through 关联的情况下,明确设置 :inverse_of 选项以优化关联查找非常重要。这有助于避免在验证过程中进行不必要的数据库查询。

有关更多详细信息,请查看双向关联文档

如果您想确保关联既存在又有效,您还需要使用 validates_associated。有关更多信息,请参阅validates_associated 部分

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

由于 false.present? 为 false,如果您想验证布尔字段不存在,您应该使用

validates :field_name, exclusion: { in: [true, false] }

默认错误消息是 _"must be blank"_。

2.2. acceptance

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

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

此检查仅在 terms_of_service 不为 nil 时执行。此验证的默认错误消息是 _"must be accepted"_。您还可以通过 message 选项传入自定义消息。

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { message: "must be agreed to" }
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.3. confirmation

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

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

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

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

此检查仅在 email_confirmation 不为 nil 时执行。要要求确认,请务必为确认属性添加一个存在检查(我们将在本指南后面查看 presence 检查)

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

此验证器的默认错误消息是 _"doesn't match confirmation"_。您还可以通过 message 选项传入自定义消息。

通常在使用此验证器时,您会希望将其与 :if 选项结合使用,以便仅在初始字段更改时才验证 "_confirmation" 字段,而**不是**每次保存记录时都验证。有关条件验证的更多信息将在后面介绍。

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

2.4. comparison

此验证器将验证任意两个可比较值之间的比较。

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

此验证器的默认错误消息是 _"failed comparison"_。您还可以通过 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.5. 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。

默认错误消息是 _"is invalid"_。

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

2.6. inclusionexclusion

这两个验证器都验证属性的值是否包含或排除在给定集合中。该集合可以是任何可枚举对象,例如数组、范围或使用 proc、lambda 或符号动态生成的集合。

  • inclusion 确保值存在于集合中。
  • exclusion 确保值**不**存在于集合中。

在这两种情况下,选项 :in 接收值集合,并且 :within 可以用作别名。有关自定义错误消息的完整选项,请参阅消息文档

如果可枚举对象是数字、时间或日期时间范围,则使用 Range#cover? 执行测试;否则,使用 include?。使用 proc 或 lambda 时,将验证实例作为参数传递,从而实现动态验证。

2.6.1. 示例

对于 inclusion

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

对于 exclusion

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

这两个验证器都允许通过返回可枚举对象的方法使用动态验证。这是一个使用 proc 进行 inclusion 的示例

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: ->(coffee) { coffee.available_sizes } }

  def available_sizes
    %w(small medium large extra_large)
  end
end

同样,对于 exclusion

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: ->(account) { account.reserved_subdomains } }

  def reserved_subdomains
    %w(www us ca jp admin)
  end
end

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 属性长度必须包含在给定区间内。此选项的值必须是一个范围。
: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

默认错误消息是复数(例如“is too short (minimum is %{count} characters)”)。因此,当 :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 的默认错误消息是 _"must be an integer"_。

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

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

未指定选项时的默认错误消息是 _"is not a number"_。

还有许多选项可用于对可接受值添加约束

选项 描述 默认错误消息
: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
person = Person.new(name: "Alice", login: "alice123", email: "alice@example.com")
person.valid?
=> true # presence validation passes

invalid_person = Person.new(name: "", login: nil, email: "bob@example.com")
invalid_person.valid?
=> false # presence validation fails

要检查关联是否存在,您需要测试关联对象是否存在,而不是用于映射关联的外键。测试关联将帮助您确定外键不为空并且引用的对象存在。

class Supplier < ApplicationRecord
  has_one :account
  validates :account, presence: true
end
irb> account = Account.create(name: "Account A")

irb> supplier = Supplier.new(account: account)
irb> supplier.valid?
=> true # presence validation passes

irb> invalid_supplier = Supplier.new
irb> invalid_supplier.valid?
=> false # presence validation fails

在您使用自定义 :foreign_key:through 关联的情况下,明确设置 :inverse_of 选项以优化关联查找非常重要。这有助于避免在验证过程中进行不必要的数据库查询。

有关更多详细信息,请查看双向关联文档

如果您想确保关联既存在又有效,您还需要使用 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. 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 片段以限制唯一性约束查找

validates :name, uniqueness: { conditions: -> { where(status: "active") } }

默认错误消息是 _"has already been taken"_。

有关更多信息,请参阅 validates_uniqueness_of

2.11. 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.12. 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.13. validates_with

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

class AddressValidator < ActiveModel::Validator
  def validate(record)
    if record.house_number.blank?
      record.errors.add :house_number, "is required"
    end

    if record.street.blank?
      record.errors.add :street, "is required"
    end

    if record.postcode.blank?
      record.errors.add :postcode, "is required"
    end
  end
end

class Invoice < ApplicationRecord
  validates_with AddressValidator
end

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

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

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

如果您想在特定属性上添加错误,可以将其作为 add 方法的第一个参数传递。

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 AddressValidator < ActiveModel::Validator
  def validate(record)
    options[:fields].each do |field|
      if record.send(field).blank?
        record.errors.add field, "is required"
      end
    end
  end
end

class Invoice < ApplicationRecord
  validates_with AddressValidator, fields: [:house_number, :street, :postcode, :country]
end

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

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

class Invoice < ApplicationRecord
  validate do |invoice|
    AddressValidator.new(invoice).validate
  end
end

class AddressValidator
  def initialize(invoice)
    @invoice = invoice
  end

  def validate
    validate_field(:house_number)
    validate_field(:street)
    validate_field(:postcode)
  end

  private
    def validate_field(field)
      if @invoice.send(field).blank?
        @invoice.errors.add field, "#{field.to_s.humanize} is required"
      end
    end
end

我们稍后将更详细地介绍自定义验证

3. 验证选项

验证器支持几个常用选项。这些选项是

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

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

3.1. :allow_nil

当被验证的值为 nil 时,:allow_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: 6 }, allow_blank: true
end
irb> Topic.create(title: "").valid?
=> true
irb> Topic.create(title: nil).valid?
=> true
irb> Topic.create(title: "short").valid?
=> false # 'short' is not of length 6, so validation fails even though it's not blank

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

要翻译错误消息,请参阅 I18n 指南

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. 条件验证

有时,只有当满足给定条件时才验证对象才有意义。您可以通过使用 :if:unless 选项来实现,这些选项可以接受符号、ProcArray。当您想要指定验证**应该**何时发生时,可以使用 :if 选项。或者,如果您想指定验证**不应该**何时发生,那么您可以使用 :unless 选项。

4.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

4.2. 将 Proc 与 :if:unless 一起使用

可以将 :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? }

4.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? 合并到其选项中。

4.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 时,验证才会运行。

5. 严格验证

您还可以将验证指定为严格验证,并在对象无效时引发 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

6. 列出验证器

如果您想找出给定对象的所有验证器,可以使用 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. 执行自定义验证

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

7.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 方法,该方法接受三个参数:record、attribute 和 value。它们分别对应于实例、要验证的属性以及传入实例中属性的值。

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

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

7.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 的更多详细信息,请参阅上面一节。

7.3. 自定义上下文

您可以为回调定义自己的自定义验证上下文,这在您希望根据特定场景执行验证或将某些回调分组并将其在特定上下文中运行时非常有用。自定义上下文的常见场景是当您有一个多步表单并希望按步骤执行验证时。

例如,您可以为表单的每个步骤定义自定义上下文

class User < ApplicationRecord
  validate :personal_information, on: :personal_info
  validate :contact_information, on: :contact_info
  validate :location_information, on: :location_info

  private
    def personal_information
      errors.add(:base, "Name must be present") if first_name.blank?
      errors.add(:base, "Age must be at least 18") if age && age < 18
    end

    def contact_information
      errors.add(:base, "Email must be present") if email.blank?
      errors.add(:base, "Phone number must be present") if phone.blank?
    end

    def location_information
      errors.add(:base, "Address must be present") if address.blank?
      errors.add(:base, "City must be present") if city.blank?
    end
end

在这些情况下,您可能会倾向于完全跳过回调,但定义自定义上下文可能是一种更结构化的方法。您需要将上下文与 :on 选项结合使用,以为回调定义自定义上下文。

定义自定义上下文后,您可以使用它来触发验证

irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St")
irb> user.valid?(:personal_info) # => false
irb> user.valid?(:contact_info) # => true
irb> user.valid?(:location_info) # => false

您还可以使用自定义上下文来触发任何支持回调的方法上的验证。例如,您可以使用自定义上下文来触发 save 上的验证

irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St")
irb> user.save(context: :personal_info) # => false
irb> user.save(context: :contact_info) # => true
irb> user.save(context: :location_info) # => false

8. 处理验证错误

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

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

8.1. errors

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}

8.2. errors[]

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

此方法仅在验证运行**之后**才有用,因为它只检查 errors 集合,并且不会触发验证本身。它与上面解释的 ActiveRecord::Base#invalid? 方法不同,因为它不验证整个对象的有效性。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)"]

8.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 3

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

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 指南。)

8.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.new
irb> person.errors.where(:name).first.type
=> :too_plain
irb> person.errors.where(:name).first.full_message
=> "Name is not cool enough"

8.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.new
irb> person.errors.where(:base).first.full_message
=> "This person is invalid because ..."

8.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: "andrea@example.com")
irb> person.valid?
=> true
irb> person.errors.size
=> 0

8.7. errors.clear

当您有意清除 errors 集合时,使用 clear 方法。当然,在一个无效对象上调用 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

9. 在视图中显示验证错误

定义模型并添加验证后,您会希望在通过 Web 表单创建该模型时验证失败时显示错误消息。

由于每个应用程序处理验证错误显示的方式不同,Rails 不包含任何用于生成这些消息的视图助手。但是,Rails 为您提供了大量与验证交互的方法,您可以用来构建自己的验证。此外,在生成脚手架时,Rails 会将一些生成的 ERB 放入 _form.html.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 像素的红色边框。

9.1. 自定义错误字段包装器

Rails 使用 field_error_proc 配置选项将带有错误的字段包装在 HTML 中。默认情况下,此选项将错误的表单字段包装在带有 field_with_errors 类的 <div> 中,如上例所示

config.action_view.field_error_proc = Proc.new { |html_tag, instance| content_tag :div, html_tag, class: "field_with_errors" }

您可以通过修改应用程序配置中的 field_error_proc 设置来定制此行为,允许您更改错误在表单中的呈现方式。有关更多详细信息,请参阅配置指南中的 field_error_proc



回到顶部