Active Record 数据验证
数据验证简介
为什么要做数据验证?
数据从客户端向数据库流动的各个环节均可以进行数据验证,但是各有优劣之处。
客户端验证
客户端的验证更多的是为用户提供了实时反馈,提示用户修改数据。用作安全性验证时,客户端验证并不可靠,例如通过禁用 JavaScript 可以跳过浏览器的验证。
控制器验证
控制器的设计目的是接受并处理数据,它虽然可以同时进行数据验证,但过多的验证会使逻辑变得混乱、代码变得混乱。控制器可能会将数据分发到各个模型进行保存,多个控制器也可能调用相同的模型操纵数据,此时控制器层面上的数据验证就会变得复杂冗余。
模型验证
Rails 中期望的数据验证方式,在定义模型时定义数据验证方式,数据验证直接绑定到模型的各个属性上。只有通过验证的数据才能存入数据库,数据验证能够与数据库的类型解绑。基于模型的验证最常用。
数据库的内建约束
依赖于数据库的类型,维护困难,适用情景较少,一般不推荐使用。
什么时候做数据验证?
数据对象的两种状态:已经存入数据库中、只在内存中尚未存入数据库。可以使用实例方法new_record?
进行判断。
基于模型的数据验证发生在 INSERT 和 UPDATE 语句执行之前,用来判断是否持久化到数据库。
会进行验证的方法有create
, update
和save
以及它们的严格版本(带!
后缀的方法)。在宽松方法下如果未通过验证,create
会返回对象本身,update
&save
会返回false
;在严格方法下如果未通过验证,会抛出ActiveRecord::RecordInvalid
异常。
跳过验证
不管验证是否通过都会更新数据库的方法有:
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
save(validate: false)
valid? 和 invalid?
实例方法valid?
用来检查数据对象是否合法,它会触发数据验证。如果验证失败会将错误信息保存到实例属性.errors.message
中。
errors.message
只有在数据验证触发时才会更新,所以新建的数据对象的errors.message
是一个空哈希,不管它合不合法。
errors.message
会按属性名保存错误信息,通过实例属性.errors
可以提取各个属性的报错信息。
errors[]
在数据验证之后,使用实例属性.errors[:attribute]
来查看某属性相关的错误信息,没有错误时为空数组。
该方式只收集错误信息,不会触发数据验证。
数据验证帮助方法
Rails 提供了大量可直接使用的验证方法。
validate
acceptance
略。
validates_associated
验证关联模型,会在每个关联对象上依次调用valid?
方法。
class Library < ActiveRecord::Base
has_many :books
validates_associated :books
end
如果两个模型相互关联,只能在一个模型上定义validates_associated
,否则会陷入死循环。
错误消息会保存到每个关联对象的errors.message
中,而不是集中在当前对象上。
confirmation
略。
exclusion
检查属性的值是否不在指定的集合中。
class Account < ActiveRecord::Base
validates :subdomain, exclusion: {
in: %w[www us ca jp],
message: "%{value} is reserved"
}
end
exclusion
选项中的:in
可以换成:with
。:message
中使用%{value}
获取属性的值。
format
正则匹配。
class Product < ActiveRecord::Base
validates :legacy_code, format: {
with: /\A[a-zA-Z]+\z/,
message: "only allow letters"
}
end
inclusion
与exclusion
相似,检查属性的值是否在指定的集合中,使用:in
或者:within
。
length
验证属性值的长度。可选的选项有
:minimum
:maximum
:in
or:within
:指定范围:is
定制消息也有很多可用的模板变量,此处略。
numericality
是否只包含数值。选项有
:only_integer
: bool, 需为整数:greater_than
: number, 需比指定值大:greater_than_or_equal_to
:equal_to
:less_than
:less_than_or_equal_to
:odd
: bool, 需为奇数:even
: bool, 需为偶数
presence
将:presence
设为true
保证属性值不为nil
或者空字符串。该方法只能用来验证非布尔类型的属性。
如果测试的是关联的对象,则需要关联的对象存在:
class LineItem < ActiveRecord::Base
belongs_to :order
validates :orders, presence: true
end
同时关联模型上需要使用:inverse_of
进行声明:
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
验证布尔类型的属性值是否存在需要使用inclusion
:
class Order < ActiveRecord::Base
validates :done, inclusion: {in: [true, false]}
uniqueness
不是很安全的唯一性约束,检查数据表中是否已经存在该属性值。但是当多个连接存入同一个值时,数据表中还是可能会出现相同的值,要保证绝对的唯一,需要在数据库层面设置唯一性约束。
:scope
选项指定一个字段,用作检验的范围。在指定字段值的子表中检验是否已经存在该值,此时数据表中可能会存在多个相同的属性值。
:case_sensitive
指定是否大小写敏感,默认为true
。
class Holiday < ActiveRecord::Base
validates :email, uniqueness: true
validates :name, uniqueness: {
scope: :year,
case_sensitive: false,
message: "should happen once per year"
}
end
validates_with
使用自定义的数据验证类对数据对象进行验证。验证类需继承ActiveModel::Validator
基类,实现其validate
方法,并将错误信息写入errors
中。validate
方法形式如下:
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any? { |field| record.send(field) == "Evil" }
record.errors[:base] << "This person is evil"
end
end
end
class Person < ActiveRecord::Base
validates_with GoodnessValidator, fields: %w[first_name last_name]
end
validate
函数只有一个参数record
,表示数据对象validate
的其他参数保存在options
中
验证类在整个程序生存周期中只初始化一次,在validate
方法中使用实例对象时最好是在构造函数中显示声明:
class GoodnessValidator < ActiveModel::Validator def initialize(person) @person = person end def validate # use `@person` as the datum instance instead of `record` endend
validates_each
将多个属性传入代码块依次进行验证,格式为
class Person < ActiveRecord::Base validates_each %w[name surname] do |record, attr, value| # record 是数据对象,attr 是属性名称,value 是属性值 # 验证失败时将错误信息添加到 record.errors 中即可 endend
常用的验证选项
:allow_nil
允许验证的值为nil
。
class Coffee < ActiveRecord::Base validates :size, allow_nil: trueend
:allow_blank
允许验证的值为nil
或者空字符串。
class Topic < ActiveRecord::Base validates :title, length: {is: 5}, allow_blank: trueend
:message
略。
:on
在何时进行数据验证,内建的验证方法总是在数据创建和更新时自动进行数据验证,即调用save
, create
, update
方法时。
:on
可选值有:create
和:update
。
class Person < ActiveRecord::Base validates :email, uniqueness: true, on: :create validates :age, numericality: true, on: :update validates :name, presence: true # both :create and :updateend
严格验证
在严格模式下,验证失败会抛出异常。
将:strict
选项设置为true
启用严格模式。
默认抛出ActiveModel::StrictValidationFailed
异常。
将:strict
选项指定为特定的异常类还可以抛出特定类型的异常。
class Person < ActiveRecord::Base validates :token, presence: true, uniqueness: true, strict: TokenGenerationExceptionend
条件验证
满足特定的条件才进行验证,使用:if
or :unless
设置验证的前提条件。
指定 Symbol
当:if
or :unless
的值为 Symbol 时,表示执行一个方法。
class Order < ActiveRecord::Base validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? payment_type == "card" endend
指定字符串
当:if
or :unless
的值为字符串时,该字符串会自动使用eval
执行,因此该字符串必须是一串 Ruby 代码,适用于代码非常短时。
class Order < ActiveRecord::Base validates :surname, presence: true, if: "name.nil?"end
指定 Proc
没看懂,略。
条件组合
使用with_options
将:if
or :unless
条件用在多个属性的验证上
class User < ActiveRecord::Base with_options if: :is_admin? do |admin| admin.validates :password, length: {minimu: 10} admin.validates :email, presence: true endend
联合条件
使用多个条件验证同一个属性。
class Computer < ActiveRecord::Base validates :mouse, presence: true, if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? }end
自定义验证方式
自定义类进行验证
参考validates_with
的用法,为整个数据对象指定一个验证类。
class MyValidator < ActiveModel::Validator def validate(record) # options # record.errors endendclass Person < ActiveRecord::Base validates_with MyValidatorend
也可以为单个属性定义验证类,该类名称与属性对应,继承自ActiveModel::EachValidator
基类,实现validate_each
方法。在模型中使用与属性同名的选项启用属性验证类。
class EmailValidator < ActiveModel::EachValidator def validate_each(record, attr, value) endendclass Person < ActiveRecord::Base validates :email, presence: true, email: trueend
自定义方法进行验证
可以将实例方法方法设为数据验证的方法,区分验证方法和普通方法的方式是:使用实例的validate
方法注册哪些方法是用于数据验证的方法。在数据验证方法中,如果验证失败,只需将错误信息保存到实例的errors
属性中即可。
class Invoice < ActiveRecord::Base 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, "cannot be in the past") end end def discount_cannot_be_greater_than_total_value if discount > total_value errors.add(:discount, "cannot be greater than total value") end endend
validate
注册数据验证方法时还可以使用:on
选项指定何时进行验证,可选值为:create
or :update
。
处理验证错误
对实例errors
集合的处理,最简单的方式是使用valid?
or invalid?
查看验证是否通过,完整的处理方法可以在ActiveModel::Errors
中查询到。
errors & errors[]
实例的errors
属性是一个哈希,键是每个属性的名字,值是错误消息字符串的数组。
errors.add
.add
手动添加错误消息,例如.errors.add(:name, "cannot balabala")
or .errors[:name] = "cannot balabala"
。.full_message
显示友好的错误消息,将属性名和错误消息进行组合。
errors[:base]
将错误消息添加到整个数据对象上,而不是具体的属性。他也是一个字符串数组。
errors.clear
清除错误消息集合,这并不会改变数据对象的合法性,因为下次调用valid?
or invalid?
时错误消息又会被添加到集合中。
errors.size
返回错误消息总数。
在视图中显示验证错误
略。
参考
https://ruby-china.github.io/rails-guides/v4.1/active_record_validations.html
评论区