4.4 模型中的校验(Validates)

    1. validates 方法
    2. errors
    3. helpers
    4. I18n
    5. Rspec

    我们将数据保存到数据库的时候,可以有两种数据校验,一种是在数据库中设定验证规则,一种是在程序中进行校验。

    Rails 为我们提供了方便的属性校验。在 [4.2.1 两个 Gem] 一节,我们介绍了ActiveRecord 中包含的两个 Gem,在数据查询和关联关系中,我们主要使用的是 arel。数据校验时,我们使用的是 ActiveModel

    4.4.2 校验方法

    4.4.2.1 常用的校验方法

    注解:

    [1]

    [2] 有其他几个选项:

    :minimum,最短长度
    :maximum,最大长度
    :in/:within,在某范围
    :is,指定长度

    [3] 也可以应用在关联关系上,如:

    1. class LineItem < ActiveRecord::Base
    2. belongs_to :order
    3. validates :order, presence: true
    4. end

    为了保持内存中引用相同地址,需要在 Order 上使用 inverse_of:

    1. class Order < ActiveRecord::Base
    2. has_many :line_items, inverse_of: :order
    3. end

    [4] 进入 console,做个试验:

    1. false.blank?
    2. => true
    3. true.blank?
    4. => false

    所以,使用 presence 判断 true/false 属性时,需要这样使用:

    1. validates :boolean_field_name, presence: true
    2. validates :boolean_field_name, exclusion: { in: [nil] }

    [5] 和 presence 一样,需要使用 inverse_of 限定关联关系,并且在判断 true/false 时:

    1. validates :boolean_field_name, absence: true
    2. validates :boolean_field_name, exclusion: { in: [true, false] }

    [6] uniqueness 有两个重要的选项。

    scope,比如:

    1. validates :number, uniqueness: { scope: : company_id }

    保存到数据库前,uniqueness 会先检索数据库是否已经存在该字段的值,scope 可以使检索时附带一个字段,比如:不同的公司,可以有相同的订单号,而同公司订单号必须唯一。

    默认是 true,区分大小写。改为 false,可不区分大小写。

    4.4.2.2 校验方法中的选项

    选项 含义 例子
    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]

    自定义异常类

    1. class Person < ActiveRecord::Base
    2. validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
    3. end
    4. Person.new.valid?
    5. => TokenGenerationException: Token can't be blank

    在将数据保存到数据库的时候,有些方法,会触发校验,有些则直接发送数据库 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 条件判断。

    1. class Order < ActiveRecord::Base
    2. validates :card_number, presence: true, if: :paid_with_card?
    3. def paid_with_card?
    4. payment_type == "card"
    5. end
    6. end

    这里使用的是方法判断,也可以直接使用字符串,比如:

    1. class Person < ActiveRecord::Base
    2. end

    或者一个代码块:

    1. class Account < ActiveRecord::Base
    2. validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? }
    3. end

    4.4.3.3 valid? 方法

    valid?invalid? 方法会触发校验。校验成功时返回 true,失败时,返回 false,并且将校验信息放入 errors 类。访问 order.errors,返回的是 ActiveModel::Errors 实例,它的代码在 这里

    4.4.4 Errors 对象

    校验失败时,model.errors 会保存入校验的属性和失败原因。我们可以通过几个方法,从 errors 实例中拿到具体的信息。

    1. % model.errors.messages
    2. => {:number=>["must be blank"]}

    messages 方法返回的是 hash 结构的信息,key 是校验的属性。

    1. % model.errors.full_messages
    2. => ["Number can't be blank", ...]

    full_messages 方法返回 Array 结构的完整错误信息。这在资源编辑的 form 页面,可以整体输出错误信息,不过它没有具体到某个属性上。对于某个属性,我们可以使用 errors[:number] 来读取:

    在某些时候,我们需要添加自己的信息,可以使用:

    1. order.errors.add(:number, "订单号不能含有 !@#%*()_-+= 等字符")

    如果添加的信息,并不一定是某个具体属性,可以添加到errors 的 base 中:

    order.errors.clear,可以清理掉所有信息.

    我们先修改一下 I18n 文件加载地址,在 application.rb 文件里,我们找到这一段:

    1. config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**/*.{rb,yml}').to_s]
    2. config.i18n.default_locale = :"zh-CN"

    这样会加载我们在 config/locales 中的全部语言包文件(注意,这里使用的是 **/*.{rb,yml})。

    我们创建语言包,为了便于维护,我在这里做了细分,大家可以在 这里 查看。

    进到终端里,测试下:

    1. % product = Product.new
    2. % product.valid?
    3. => false
    4. % product.errors.full_messages
    5. => ["名称不能为空字符"]

    在后面的章节里,会专门讲解 I18n 的问题,如果不像本例子中自己添加语言包,也可以安装 这个 gem 来解决问题。

    4.4.5.1 页面中显示错误信息

    为了让页面集中的显示错误信息,我们在 form 中使用了局部模板,把校验失败的内容显示在输入框的顶部。

    1. <% if @product.errors.any? %>
    2. <div id="error_expl" class="panel panel-danger">
    3. <div class="panel-heading">
    4. <h3 class="panel-title"><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h3>
    5. </div>
    6. <div class="panel-body">
    7. <ul>
    8. <% @product.errors.full_messages.each do |msg| %>
    9. <li><%= msg %></li>
    10. <% end %>
    11. </ul>
    12. </div>
    13. </div>
    14. <% end %>

    full_messages 返回 Array 的校验信息,我们只需循环显示即可。如果想在输入框旁边显示信息,可以单独读取该属性,比如 .errors[:name],可以放到一个 jquery 的 tooltip 中。

    不过,这种信息是要提交到服务器端处理后,才能显示出来的。为了在页面端就显示校验,我们还是需要 jQuery 插件的。

    4.4.5.2 jQuery 校验

    Form 校验的时候,有两个插件较常用。

    是较常用的一个,也很简单,但是需要在页面上显示中文,还需要它的中文插件。

    <%= javascript_include_tag ‘spree/jquery.validate/localization/messages_zh’ %>

    中文语言包的源码在[这里] (https://github.com/jzaefferer/jquery-validation/blob/master/src/localization/messages_zh.js)。

    如果不需要校验具体信息,因为我们已经使用了 bootstrap 这个前端框架,所以我们可以使用它的表单校验:

    它会按照 bootstrap 的方式,将输入框加上图标,使校验更加直观。当然,你还可以读取具体的属性信息,放到 bootstrap 的 tooltip 里。

    4.4.6 Rspec

    和上一张的关联关系一样, 也提供了方便的校验测试框架。

    1. describe Product do
    2. it { should validate_presence_of(:name) }
    3. end