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 不会执行 INSERT
或 UPDATE
操作。这避免了将无效对象存储在数据库中。您可以选择在创建、保存或更新对象时运行特定验证。
有许多方法可以更改数据库中对象的狀態。有些方法会触发验证,而有些方法则不会。这意味着如果您不小心,则有可能将对象以无效状态保存到数据库中。
以下方法触发验证,并且只有在对象有效的情况下才会将对象保存到数据库
带感叹号的版本(例如 save!
)如果记录无效,则会引发异常。不带感叹号的版本不会:save
和 update
返回 false
,而 create
返回对象。
1.3 跳过验证
以下方法跳过验证,并将无论对象是否有效都将其保存到数据库。应谨慎使用它们。请参考方法文档以了解更多信息。
decrement!
decrement_counter
increment!
increment_counter
insert
insert!
insert_all
insert_all!
toggle!
touch
touch_all
update_all
update_attribute
update_attribute!
update_column
update_columns
update_counters
upsert
upsert_all
请注意,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
实例化的对象即使在技术上无效时也不会报告错误,因为验证仅在保存对象时(例如使用 create
或 save
方法)才会自动运行。
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
选项来允许它。请注意,对于 Integer
和 Float
列,空字符串将转换为 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_one
或 has_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_one
或 has_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
选项接受 String
或 Proc
作为其值。
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 像素的红色边框。