数据验证
概览
会触发数据验证的方法:create, create!, save, save!, update, update!
会跳过验证,不管验证是否通过都会把对象存入数据库的方法:decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
vaild?方法会触发数据验证,如果对象上没有错误,返回true,否则返回false
Active Record执行验证后,所有发现的错误都可以通过实例方法errors.messages获取。该方法返回一个错误合集。
class Person < ApplicationRecord
validates :name, presence: true
end
p = Person.new
p.errors.messages
# => {}
p.valid?
# => false
p.errors.messages
# => {name:["can't be blank"]}
p = Person.create
p.errors.messages
# => {name:["can't be blank"]}
p.save
# => false
若想检查对象的某个属性是否有效,可以使用errors[:attribute]
class Person < ApplicationRecord
validates :name, presence: true
end
Person.new.errors[:name].any? # => false
Person.create.errors[:name].any? # => true
若想查看是哪个验证导致属性无效的,可以使用 errors.details[:attribute]
数据验证辅助方法
每个辅助方法都可以接受任意个属性名,所以一行代码就能在多个属性上做同一种验证。
所以辅助方法都可以指定:on和:message选项,分别指定何时做验证,一级验证失败后向errors集合添加什么消息。
1.acceptance
这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受应用的服务条款、确保用户阅读了一些文本,等等。
class Person < ApplicationRecord
vaildates :terms_of_service, acceptance: {message: 'must be abided', accept: ['yes', '1']}
end
2.validates_associated
如果模型和其他模型有关联,而且关联的模型也要验证,要使用这个辅助方法。保存对象时,会在相关联的每个对象上调用 valid? 方法。
不要在关联的两端都使用 validates_associated,这样会变成无限循环。
3.confirmation
如果要检查两个文本字段的值是否完全相同,使用这个辅助方法。例如,确认电子邮件地址或密码。这个验证创建一个虚拟属性,其名字为要验证的属性名后加 _confirmation。
在试图模板中可以这么写:
<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>
只有 email_confirmation 的值不是 nil 时才会检查。所以要为确认属性加上存在性验证
class Person < ApplicationRecord
validates :email, confirmation: true
validates :email_confirmation, presence: true
end
此外,还可以使用 :case_sensitive 选项指定确认时是否区分大小写。这个选项的默认值是 true。
4.format
这个辅助方法检查属性的值是否匹配 :with 选项指定的正则表达式。
class Product < ApplicationRecord
vaildates :legacy_code, format: { with: /\A[a-zA-Z]+/, message: 'only allow letters' }
end
或者,使用 :without 选项,指定属性的值不能匹配正则表达式。
5.inclusion、exclusion
inclusion检查属性的值是否在指定的集合中
exclusion检查属性的值是否不在指定的集合中
class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
class Account < ApplicationRecord
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
end
6.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
7.numericality
这个辅助方法检查属性的值是否只包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,把 :only_integer 选项设为 true。否则,会尝试使用 Float 把值转换成数字。
class Player < ApplicationRecord
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true }
end
除了 :only_integer 之外,这个方法还可指定以下选项,限制可接受的值:
:greater_than:属性值必须比指定的值大。该选项默认的错误消息是“must be greater than %{count}”;
:greater_than_or_equal_to:属性值必须大于或等于指定的值。该选项默认的错误消息是“must be greater than or equal to %{count}”;
:equal_to:属性值必须等于指定的值。该选项默认的错误消息是“must be equal to %{count}”;
:less_than:属性值必须比指定的值小。该选项默认的错误消息是“must be less than %{count}”;
:less_than_or_equal_to:属性值必须小于或等于指定的值。该选项默认的错误消息是“must be less than or equal to %{count}”;
:other_than:属性值必须与指定的值不同。该选项默认的错误消息是“must be other than %{count}”。
:odd:如果设为 true,属性值必须是奇数。该选项默认的错误消息是“must be odd”;
:even:如果设为 true,属性值必须是偶数。该选项默认的错误消息是“must be even”;
numericality 默认不接受 nil 值。可以使用 allow_nil: true 选项允许接受 nil。
8.uniqueness
这个辅助方法在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两次数据库连接创建的记录具有相同的字段值。为了避免出现这种问题,必须在数据库的字段上建立唯一性索引。
:scope 选项用于指定检查唯一性时使用的一个或多个属性
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
如果想确保使用 :scope 选项的唯一性验证严格有效,必须在数据库中为多列创建唯一性索引。
9.validates_with
这个辅助方法把记录交给其他类做验证。
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == 'Evil'
record.errors[:base] << 'This person is evil'
end
end
end
class Person < ApplicationRecord
validates_with GoodnessValidator
end
record.errors[:base] 中的错误针对整个对象,而不是特定的属性。
10.validates_each
这个辅助方法使用代码块中的代码验证属性。它没有预先定义验证函数,你要在代码块中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头
class Person < ApplicationRecord
validates_each :name, :surname do |record, attr, value|
record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
end
end
条件验证
有时,只有满足特定条件时做验证才说得通。条件可通过 :if 和 :unless 选项指定,这两个选项的值可以是符号、字符串、Proc 或数组。:if 选项指定何时做验证。如果要指定何时不做验证,使用 :unless 选项。
1.使用符号
:if 和 :unless 选项的值为符号时,表示要在验证之前执行对应的方法。这是最常用的设置方法。
class Order < ApplicationRecord
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
2.使用Proc
:if and :unless 选项的值还可以是 Proc。使用 Proc 对象可以在行间编写条件,不用定义额外的方法。这种形式最适合用在一行代码能表示的条件上。
class Account < ApplicationRecord
validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? }
end
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.联合条件
如果是否做某个验证要满足多个条件时,可以使用数组。而且,一个验证可以同时指定 :if 和 :unless 选项
class Computer < ApplicationRecord
validates :mouse, presence: true,
if: ["market.retail?", :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
只有当 :if 选项的所有条件都返回 true,且 :unless 选项中的条件返回 false 时才会做验证。
自定义验证
1.自定义验证类
自定义的验证类继承自 ActiveModel::Validator,必须实现 validate 方法,其参数是要验证的记录,然后验证这个记录是否有效。自定义的验证类通过 validates_with 方法调用。
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
在自定义的验证类中验证单个属性,最简单的方法是继承 ActiveModel::EachValidator 类。此时,自定义的验证类必须实现 validate_each 方法。这个方法接受三个参数:记录、属性名和属性值。它们分别对应模型实例、要验证的属性及其值。
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ApplicationRecord
validates :email, presence: true, email: true
end
2.自定义验证方法
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