通知短信+运营短信,5秒速达,支持群发助手一键发送🚀高效触达和通知客户 广告
# 4.4 模型中的校验(Validates) ## 概要: 本课时讲解 Model 中的属性校验方法,以及在页面上显示校验失败信息。 ## 知识点: 1. validates 方法 1. errors 1. helpers 1. I18n 1. Rspec ## 正文 ### 4.4.1 数据校验 我们将数据保存到数据库的时候,可以有两种数据校验,一种是在数据库中设定验证规则,一种是在程序中进行校验。 Rails 为我们提供了方便的属性校验。在 [4.2.1 两个 Gem] 一节,我们介绍了ActiveRecord 中包含的两个 Gem,在数据查询和关联关系中,我们主要使用的是 arel。数据校验时,我们使用的是 [ActiveModel](https://github.com/rails/rails/tree/master/activemodel)。 ### 4.4.2 校验方法 #### 4.4.2.1 常用的校验方法 | 方法 | 含义 | 例子 | |-----|-----|-----| | acceptance | 必须接受选项,比如注册条款(必须同意) | validates :terms_of_service, acceptance: true | | validates_associated | 校验关联资源,仅在关联的一端使用即可,避免循环校验 | [1] | | confirmation | 填写确认 | validates :email, confirmation: true | | exclusion | 排除内容,如某些保留关键词不允许注册使用 | validates :subdomain, exclusion: { in: %w(www us ca jp), message: "%{value} is reserved." } | | format | 格式化,如邮件格式 | validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/, message: "only allows letters" } | | inclusion | 包含内容,如特定的输入内容 | validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" } | | length | 内容长度 | validates :name, length: { minimum: 2 } [2] | | numericality | 仅数字 | validates :points, numericality: true | | presence | 必填,使用 `blank?` 方法判断 | validates :name, :login, :email, presence: true [3] [4] | | absence | 必空,使用 `present?` 判断 | [5] | | uniqueness | 唯一 | validates :email, uniqueness: true [6] | 注解: [1] ~~~ class Library < ActiveRecord::Base has_many :books validates_associated :books end ~~~ [2] 有其他几个选项: :minimum,最短长度:maximum,最大长度:in/:within,在某范围:is,指定长度 [3] 也可以应用在关联关系上,如: ~~~ class LineItem < ActiveRecord::Base belongs_to :order validates :order, presence: true end ~~~ 为了保持内存中引用相同地址,需要在 Order 上使用 inverse_of: ~~~ class Order < ActiveRecord::Base has_many :line_items, inverse_of: :order end ~~~ [4] 进入 console,做个试验: ~~~ false.blank? => true true.blank? => false ~~~ 所以,使用 presence 判断 true/false 属性时,需要这样使用: ~~~ validates :boolean_field_name, presence: true validates :boolean_field_name, inclusion: { in: [true, false] } validates :boolean_field_name, exclusion: { in: [nil] } ~~~ [5] 和 presence 一样,需要使用 inverse_of 限定关联关系,并且在判断 true/false 时: ~~~ validates :boolean_field_name, absence: true validates :boolean_field_name, exclusion: { in: [true, false] } ~~~ [6] uniqueness 有两个重要的选项。 scope,比如: ~~~ validates :number, uniqueness: { scope: : company_id } ~~~ 保存到数据库前,uniqueness 会先检索数据库是否已经存在该字段的值,scope 可以使检索时附带一个字段,比如:不同的公司,可以有相同的订单号,而同公司订单号必须唯一。 ~~~ validates :name, uniqueness: { case_sensitive: false } ~~~ 默认是 true,区分大小写。改为 false,可不区分大小写。 #### 4.4.2.2 校验方法中的选项 在检验方法 validates 中,可以使用几个选项: | 选项 | 含义 | 例子 | |-----|-----|-----| | allow_nil | 是否允许为 nil | validates :size, allow_nil: true | | allow_blank | 是否允许为 blank?,为 false 时,不可填写 `""`, `false`, `nil` | validates :title, allow_blank: true | | message | 自定义错误信息 | validates :subdomain, exclusion: { in: %w(www us ca jp), message: "%{value} 为保留关键词" } | | on | 选择在 create 或 update 上使用校验 | validates :email, uniqueness: true, on: :create | | strict | 校验失败时抛出异常,或自定异常类 | validates :name, presence: { strict: true } [1] | 注解 [1] 自定义异常类 ~~~ class Person < ActiveRecord::Base validates :token, presence: true, uniqueness: true, strict: TokenGenerationException end Person.new.valid? => TokenGenerationException: Token can't be blank ~~~ ### 4.4.3 触发校验方法 在将数据保存到数据库的时候,有些方法,会触发校验,有些则直接发送数据库 sql 命令,不触发校验。 #### 4.4.3.1 触发校验的方法 - create - create! - save - save! - update - update! `!` 结尾的方法,在校验失败时,会抛出异常。`save(validate: false)` 可以跳过 save 方法的校验。 #### 4.4.3.2 不触发校验的方法 - decrement! - decrement_counter - increment! - increment_counter - toggle! - touch - update_all - update_attribute - update_column - update_columns - update_counters #### 4.4.3.2 有条件的校验 我们可以在校验中增加 `:if` 或 `:unless` 条件判断。 ~~~ class Order < ActiveRecord::Base validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? payment_type == "card" end end ~~~ 这里使用的是方法判断,也可以直接使用字符串,比如: ~~~ class Person < ActiveRecord::Base validates :surname, presence: true, if: "name.nil?" end ~~~ 或者一个代码块: ~~~ class Account < ActiveRecord::Base validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? } end ~~~ #### 4.4.3.3 `valid?` 方法 `valid?` 和 `invalid?` 方法会触发校验。校验成功时返回 true,失败时,返回 false,并且将校验信息放入 errors 类。访问 `order.errors`,返回的是 `ActiveModel::Errors` 实例,它的代码在 [这里](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/errors.rb)。 ### 4.4.4 Errors 对象 校验失败时,model.errors 会保存入校验的属性和失败原因。我们可以通过几个方法,从 errors 实例中拿到具体的信息。 ~~~ % model.errors.messages => {:number=>["must be blank"]} ~~~ `messages` 方法返回的是 hash 结构的信息,key 是校验的属性。 ~~~ % model.errors.full_messages => ["Number can't be blank", ...] ~~~ `full_messages` 方法返回 Array 结构的完整错误信息。这在资源编辑的 form 页面,可以整体输出错误信息,不过它没有具体到某个属性上。对于某个属性,我们可以使用 `errors[:number]` 来读取: ~~~ % order.errors[:number] => ["can't be blank"] ~~~ 在某些时候,我们需要添加自己的信息,可以使用: ~~~ order.errors.add(:number, "订单号不能含有 !@#%*()_-+= 等字符") ~~~ 如果添加的信息,并不一定是某个具体属性,可以添加到errors 的 base 中: ~~~ order.errors.add(:base, "订单格式不正确") ~~~ `order.errors.clear`,可以清理掉所有信息. ### 4.4.5 使用中文的校验信息 我们已经注意到了,目前所有的校验信息都是英文的,虽然可以在自定义信息里写入中文(Not Rails Style),但是我们可以利用 Rails 提供的 I18n gem,实现文本内容的汉化。这包括异常信息。 我们先修改一下 I18n 文件加载地址,在 application.rb 文件里,我们找到这一段: ~~~ config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**/*.{rb,yml}').to_s] config.i18n.default_locale = :"zh-CN" ~~~ 这样会加载我们在 config/locales 中的全部语言包文件(注意,这里使用的是 `**/*.{rb,yml}`)。 我们创建语言包,为了便于维护,我在这里做了细分,大家可以在 [这里](https://github.com/liwei78/rails-practice-code/tree/master/chapter_4/shop/config/locales) 查看。 进到终端里,测试下: ~~~ % product = Product.new % product.valid? => false % product.errors.full_messages => ["名称不能为空字符"] ~~~ 在后面的章节里,会专门讲解 I18n 的问题,如果不像本例子中自己添加语言包,也可以安装 [rails-i18n](https://github.com/svenfuchs/rails-i18n) 这个 gem 来解决问题。 #### 4.4.5.1 页面中显示错误信息 为了让页面集中的显示错误信息,我们在 form 中使用了局部模板,把校验失败的内容显示在输入框的顶部。 ~~~ <% if @product.errors.any? %> <div id="error_expl" class="panel panel-danger"> <div class="panel-heading"> <h3 class="panel-title"><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h3> </div> <div class="panel-body"> <ul> <% @product.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> </div> <% end %> ~~~ `full_messages` 返回 Array 的校验信息,我们只需循环显示即可。如果想在输入框旁边显示信息,可以单独读取该属性,比如 `@product.errors[:name]`,可以放到一个 jquery 的 tooltip 中。 不过,这种信息是要提交到服务器端处理后,才能显示出来的。为了在页面端就显示校验,我们还是需要 jQuery 插件的。 #### 4.4.5.2 jQuery 校验 Form 校验的时候,有两个插件较常用。 [http://jqueryvalidation.org/](http://jqueryvalidation.org/) 是较常用的一个,也很简单,但是需要在页面上显示中文,还需要它的中文插件。 <%= javascript_include_tag 'spree/jquery.validate/localization/messages_zh' %> 中文语言包的源码在[这里] ([https://github.com/jzaefferer/jquery-validation/blob/master/src/localization/messages_zh.js)。](https://github.com/jzaefferer/jquery-validation/blob/master/src/localization/messages_zh.js)。) 如果不需要校验具体信息,因为我们已经使用了 bootstrap 这个前端框架,所以我们可以使用它的表单校验:[http://bootstrapvalidator.com](http://bootstrapvalidator.com) 它会按照 bootstrap 的方式,将输入框加上图标,使校验更加直观。当然,你还可以读取具体的属性信息,放到 bootstrap 的 tooltip 里。 ### 4.4.6 Rspec 和上一张的关联关系一样,[shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers#activemodel-matchers) 也提供了方便的校验测试框架。 ~~~ describe Product do it { should validate_presence_of(:name) } end ~~~ 现在我们给 Model 增加了越来越多的内容,为了方便找到方法,我们可以对代码进行一个简单的分割,这样就不会在测试和对应的业务代码间切换浪费时间了。 ~~~ # extends ................................................................... # includes .................................................................. # security .................................................................. # relationships ............................................................. # validations ............................................................... # callbacks ................................................................. # scopes .................................................................... # additional config ......................................................... # class methods ............................................................. # public instance methods ................................................... # protected instance methods ................................................ # private instance methods .................................................. ~~~