Rails Rails 5 新功能 - 实现适用于不同场景的 ActiveRecord 验证

huacnlee · 2016年07月07日 · 最后由 StephenZzz 回复于 2019年05月16日 · 7328 次阅读

ActiveRecord 的 Validation 用于实现表单验证非常方便,但某些时候它却会带来一些烦恼,因为使用不当会导致你的程序内部保存数据的时候过不了验证。

我们来看这样一个例子

最简单、常见的验证我们可能会写成这样

class User < ApplicationRecord
  validates :name, :email, :password, :id_card, presence: true
  validates :name, length: { in: 2..10 }
end

我们分别有:注册个人设置 两个表单

注册表单

由于为了能让用户尽快注册,没要求用户一定要填写 nameid_card,而 emailpassword 要求必填

个人设置表单

要求 nameid_card 必填,这个表单没有密码,所以密码是不要求必填的

问题来了

由于上面的 Validation 声明,导致注册表单没有 nameid_card,验证无法通过。

在 Rails 4 里面我们可能会用 on: :createon: :update 来缓解,但某些更复杂,更多需要验证的场景里面,update 都是有好多情况的,比如有个流程:

我们需要用户填写完成 a1 - a10,这些字段都有不同的验证条件,而且都是必填项!

  • Step 1 - 填写 a1 - a3 字段
  • Step 2 - 填写 a4 - a6 字段
  • Step 3 - 填写 a7 - a9 字段
  • Step 4 - 填写 a10 字段

这些字段都在同一个 Model,Step 1 可能可以用 on: :create 来实现不同的验证,但到了 2、3、4 部,都是同一个 Model 的 update 动作。

验证无法直接在 Model 里面写了... 😣

于是我们一般都会单独写针对不同步骤的验证函数...或者更土鳖 😨 的在 Controller 里面直接验证...

def step1
  @issue.errors.add('a1', '不能为空')  if ...
  @issue.errors.add('a1', '的长度要求 10 - 20 个字符')  if ...
  @issue.errors.add('a2', '不是一个有效的手机号')  if ...
end

def step2
  @issue.errors.add('a4', '身份证格式不正确')  if ...
  @issue.errors.add('a5', '至少要选择一项')  if ...
end

Rails 5 - Validation Custom Contexts

Rails 5 给我们带了解决方案 🎉validates:on 参数可以自定义上下文名称了

http://guides.rubyonrails.org/active_record_validations.html#on

class User < ApplicationRecord
  validates :email, :password, presence: true, on: :sign_up
  validates :password, confirmation: true, on: :sign_up
  validates :email, format: { with: /.../ }, on: :sign_up

  validates :name, :id_card, presence: true, on: :setting
  validates :name, length: { in: 2..10 }, on: :setting
end

class Issue < ApplicationRecord
  validates :a1, :a2, :a3, presence: true, on: :step1
  validates :a1, length: { in: 10..20 }, on: :step1
  validates :a2, format: { with: /.../ }, on: :step1


  validates :a4, :a5, :a6, presence: true, on: :step2
  validates :a4, format: { with: /.../ }, on: :step2

  validates :a7, :a8, :a9, presence: true, on: :step3

  validates :a10, presence: true, on: :step4
end

def step1
  @issue.save(context: :step1)
end

def step2
   @issue.save(context: :step2)
end

延伸阅读

2 楼 已删除

之前就是用自定义验证写了好多 errors.add 在 model 里....

感觉这个跟 ecto 的 changeset 简直没得比啊,只是在一条错误的道路上越走越远 https://hexdocs.pm/ecto/Ecto.Changeset.html

我记得最早 3.2 的时候就有了,然后on: :sign_up可以用with_options包起来:

with_options on: :sign_up do
  validates :email, :password, presence: true
  validates :password, confirmation: true
  validates :email, format: { with: /.../ }
end

以前这个场景是用一个 gem grouped_validations 实现,DSL 如下:

validation_group :name, :if => :ready? do
  validates_presence_of :first_name
  validates_presence_of :last_name
end

不过好久没更新了,想不到这个 feature 变成刚需特性给 rails5 内置了,rails 开发真心爽快

#6 楼 @huacnlee with_options 并没有限制传什么参数吧,我贴的代码段是经过运行验证的,从源码里也没看到有什么限制: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/option_merger.rb

然后 on 参数在内部也是给转换成一个 if 来实现: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations.rb#L162

#6 楼 @huacnlee 我没说有限制啊

我是说那个东西和新的用法不一样,新的方式只需要起一个 Context 别名就好了,无需搞一些函数做判断,或者定义虚拟字段什么的,简洁

就是 context 呗,这个功能以前就有,valid? 也可以传 context,只是 save 不行。现在 Rails 把它做得更完善了而已。如果 Rails 4.x 要做针对某个 domain 的校验就得这么写:

# Trigger domain validations only
if model.valid?(on: :some_domain)
  # Skip common validations
  model.save(validate: false)
end

不过说实话,这种把所有 validations 都放到 model 里面本身方向就错误了,Rails 的做法只是在上面打补丁而已。

#1 楼 @huacnlee 抱歉没看懂你指的是什么,我只能说用with_options包起来和你示例里的代码是等价的,我也没提到什么判断,虚拟字段什么的。

自动的 validate 不是只有在调用 save 等方法的时候进行的么,这时候如果用了 transactions,到了 step4 才出错退出事务的话不会有锁表之类影响么

很贴心的改进,以前这种操作我都是用 if, 然后自定义字段来做过滤的。。

抓紧升级 rails5

终于有这个功能了,好感动。

laravel 在 controller 中进行 validate 更加灵活

#1 楼 @kikyous rails 的对比,何必要拿 php 对比呐。 9 楼后面都是 1 楼。。。

#1 楼 @didme 我只是说一种方案

#4 楼 @tony612 Ecto 也有 Ecto 的问题,清晰是清晰,就是麻烦,你差不多要为每种操作都定义一个 changeset。对了,Ecto 其实就是用了 repository pattern。

#22 楼 @zillou 跟定义一堆 validation 感觉差别不大。btw,Ecto 也不是 repository pattern https://groups.google.com/forum/#!topic/elixir-ecto/CSC3ZRbJ9cU,只是刚好用了 Repo 这个名字而已

#23 楼 @tony612 我望文生义了。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号