Active Record 数据验证

数据验证概览

为什么要做数据验证

数据验证确保只有有效的数据才能存入数据库,在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。

数据验证的方式主要有数据库原生约束、客户端验证和控制器层验证:

  • 数据库约束无法兼容多种数据库,难以测试和维护,但是如果其他应用也要使用这个数据库,最好能够在数据库层做一些约束。

  • 客户端验证可靠性不高,但是和其他验证方式结合可以提供实时反馈

  • 控制器层验证不灵便,难以测试和维护,只要可能就应该保证控制器的代码简洁,这样才有利于长远发展

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 ,否则返回 falseinvalid? 方法则相反。执行验证之后,错误可以通过实例方法 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 返回错误消息总数。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容