数据验证概览
为什么要做数据验证
数据验证确保只有有效的数据才能存入数据库,在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。
数据验证的方式主要有数据库原生约束、客户端验证和控制器层验证:
数据库约束无法兼容多种数据库,难以测试和维护,但是如果其他应用也要使用这个数据库,最好能够在数据库层做一些约束。
客户端验证可靠性不高,但是和其他验证方式结合可以提供实时反馈
控制器层验证不灵便,难以测试和维护,只要可能就应该保证控制器的代码简洁,这样才有利于长远发展
Active Record 对象分为两种,一种在数据库中有对应记录,一种没有,新建对象还不属于数据库,只有调用了 save
方法后,才会存入数据库,可以使用 new_record?
方法判断是否存入数据库,未存入则返回 true
,存入则返回 false
新建并保存会执行 SQL INSERT
操作,更新记录会执行 SQL UPDATE
操作,一般情况下,数据验证发生在执行这些SQL语句之前,如果验证失败,对象会被标记为无效, Active Record 不会向数据库发送指令。
以下方法会触发数据验证:
create
create!
save
save!
update
update!
炸弹方法会在验证失败后抛出异常。
以下方法会跳过验证,不管验证是否通过都会把对象存入数据库:
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
同时,使用 save
方法时,如果传入 validate: false
参数,也会跳过验证。
同时,也可以使用 valid?
方法自己执行验证,如果对象上没有错误则返回 true
,否则返回 false
,invalid?
方法则相反。执行验证之后,错误可以通过实例方法 errors.message
获取,这个方法返回一个错误集合,如果为空,则说明对象是有效的。需要注意的是,如果没有验证数据,这个方法返回的也是一个空集合。
如果要验证某个属性是否有效,可以使用 errors[:attribute]
,这返回一个包含了所有错误的数组,如果没有错误则返回空数组,这个方法和 invalid?
方法不一样,这个方法不会验证整个对象,只会检查某个属性是否有错。
可以使用 errors.details[:attribute]
检查到底是哪个验证导致属性无效,这个方法返回一个由散列组成的数组。
数据验证的辅助方法
辅助方法可以直接在模型中使用,这些方法提供了常用的验证规则,验证失败就会向对象的 errors
集合中添加一个消息。
每个辅助方法都可以接受任意个属性名,所以一行代码可以在多个属性上做同一种验证。
acceptance
检查表单提交时,用户界面中的复选框是否被选中,一般用来要求用户接受应用的服务条款、确保用户阅读了一些文本等。
class Person < ApplicationRecord
validates :terms_of_service, acceptance: true
end
validates_associated
如果模型与其他模型有关联,而且关联的模型也需要验证,就是用这个方法,保存对象时,会在相关联的每个对象上调用 valid?
方法。
class Library < ApplicationRecord
has_many :books
validates_associated :books
end
不要在关联的两端使用,这样会造成无限的循环
confirmation
检查两个文本字段的值是否完全相同,如确认邮件地址或者密码。这个验证创建一个虚拟属性,其名字为要验证的属性名后加 _confirmation
。
class Person < ApplicationRecord
validates :email, confirmation: true
end
在视图模板中视图可以如下:
<%= 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
class Person < ApplicationRecord
validates :email, confirmation: {case_sensitive: false}
end
exclusion
这个方法检查属性的值是否不在指定的集合中,集合可以是任何一种可枚举的对象
class Account < Application
validates :subdomain, exclusion: {in: %w(www us ca jp), message: "%{value} is reserved"}
end
in
选项设置哪些值不能作为属性的值,in
的别名是 with
formate
这个方法检查属性的值是否匹配 :with
选项指定的正则表达式。
class Product < ApplicationReocrd
validates :legacy_code, formate: {with: /\A[a-zA-Z]+\z/, message: "only allows letters"}
end
inclusion
这个方法检查属性的值是否在指定的集合中,集合可以是任何一种可枚举的对象
class Coffee < ApplicationRecord
validates :size, inclusion: {in: %w(small mediun large), message: "%{value} is not a valid size"}
end
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}
表示长度限制的值
class Person < ApplicationRecord
validates :bio, length: {maximum: 1000, too_long: "%{count} characters is the maximum allowed"}
end
numericality
检查属性是否只包含数字,默认匹配的值是可选的正负符号后加整数或浮点数,如果只接受整数,把 :only_integer
选项设置为 true
,否则会使用Float把值转换为数字。
class Player < ApplicationRecord
validates :points, numericality: true
validates :games_played< numericality: {only_integer: true}
end
除此之外,这个方法还可指定以下选项:
:greater_than
:属性值需大于 >:greater_than_or_equal_to
:>=:equal_to
:=:less_than
:<:less_than_or_equal_to
:<=:other_than
:!=:odd
:必须为奇数:even
:必须为偶数
此方法默认不接受 nil
值,可以使用 allow_nil: true
选项允许接受 nil
presence
检查属性是否为非空值,方法调用 blank?
方法检查是否为 nil
或空字符串
class Person < ApplicationRecord
validates :name, :login, :email, presence: true
end
absence
验证属性值是否为空,使用 present?
方法检查是否为 nil
或空字符串
class Person < ApplicationRecord
validates :name, :login, :email, absence: true
end
uniqueness
这个方法在保存对象前验证属性值是否唯一,这个方法不会在数据库中创建唯一性约束,所以有可能两次数据库连接创建的记录具有相同的值,所以最好在数据库字段上建立唯一性约束。
class Account < ApplicationRecord
validates :email, uniqueness: true
end
这个验证会在模型对应的表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。
可以使用 :case_sensitive
选项
class Person < ApplicationRecord
validates :name, uniqueness: {case_sensitive: false}
end
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
这个方法的参数是一个类或者一组类。
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
代码块的参数是记录、属性名和属性值。
常用验证选项
:allow_nil
允许 nil
值,如果要验证的值是 nil
就跳过验证
class Coffee < ApplicationRecord
validates :size, inclusion: {in: %w(small medium large), message: "%{value} is not a valid size"}, allow_nil: true
end
:allow_blank
与上面方法类似,使用 blank?
方法判断,空字符串和nil时跳过验证
:message
添加错误消息,消息中可以包含 %{value}
、 %{attribute}
、%{model}
:on
指定验证时机,默认都在保存时验证,使用使用
on: :create
:只在创建时验证on: :update
:只在更新时验证
class Person < ApplicationRecord
# 更新时允许电子邮件地址重复
validates :email, uniqueness: true, on: :create
# 创建记录时允许年龄不是数字
validates :age, numericality: true, on: :update
# 默认行为(创建和更新时都验证)
validates :name, presence: true
end
:strict
使用严格验证模式,对象无效时抛出异常
class Person < ApplicationRecord
validates :name, presence: { strict: true }
end
Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
条件验证
使用 :if
和 :unless
选项只有满足特定条件才验证,值可以是符号、字符串、Proc或数组。
选项为符号时,表示验证之前执行对应的方法。这是最常用的设置方法。
class Order < ApplicationRecord
validates :card_number
end
自定义验证
自定义验证类继承自 ActiveModel::Validator,必须实现validate方法,参数是要验证的记录
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::Errors
的实例包含所有的错误,键是每个属性的名称,只是一个数组,包含错误消息字符串。
errors[]
用于获取某个属性上的错误消息
errors.add
用于手动添加某属性的错误消息,参数是属性和错误消息
errors.details
返回错误详情
errors.clear
清楚errors集合中的所有消息
errors.size
返回错误消息总数。