Rails ActiveModel Validation 里的 if option 到底是在哪里执行判断的。

zztczcx · 2016年10月12日 · 最后由 angelfan 回复于 2016年10月13日 · 2485 次阅读

:if - Specifies a method, proc or string to call to determine if the validation should occur (e.g. if: :allow_validation, or if: Proc.new { |user| user.signup_step > 2 }). The method, proc or string should return or evaluate to a +true+ or +false+ value.

example validates :password, presence: true, confirmation: true, if: :password_required?

翻了下 source code, 没有看到具体执行 if 判断的地方。

最有可能的地方也是 加了 on 的判断。 options.key?(:on)


看有人说 是在 callback 里检查的,但没找到。

不知道有哪位给讲一下,谢谢。

stackoverflow 有个类似的问题: http://stackoverflow.com/questions/4508732/how-does-activerecord-implement-if-on-validations

实际上, if: :password_required?if option 特性,并不是来做ActiveModel 而是来自 ActiveSupport::Callbacks。之前好像有一个帖子专门讨论callback的。

我把整个定义这个 validate 过程写下来,以Rails 5代码为例,你就能看到在哪里执行 if 了。

首先是 validates的定义 code

注意这段代码,validates_with(validator, defaults.merge(_parse_validates_options(options)))

validates_with的定义在这里 code

然后有看到了,validate(validator, options)

validate 的定义在这里 code

首先看到这里,好激动,看到了if

if options.key?(:on)
  options = options.dup
  options[:if] = Array(options[:if])
  options[:if].unshift ->(o) {
    !(Array(options[:on]) & Array(o.validation_context)).empty?
  }
end

同时下面还有一段 set_callback(:validate, *args, &block) 我们的if还在 args 里面

到这里大概明白,实际上 validate 就是一个 callback

那么set_callback是什么鬼?

网上找会看到这段代码

included do
    extend ActiveModel::Naming
    extend ActiveModel::Callbacks #这个!!这个!!
    extend ActiveModel::Translation

然后找到了ActiveModel::Callback定义的地方。code,但是,实际上这里并没有做什么。

然后又看到了,

def self.extended(base) #:nodoc:
  base.class_eval do
    include ActiveSupport::Callbacks
  end
end

终于到了ActiveSupport::Callbacks ,我们找到的是set_callback的定义

定义在这里code

其中有这段代码

mapped = filters.map do |filter|
    Callback.build(self_chain, filter, type, options)
end

然后要找的就是Callback的定义了

Callback code

又一次兴奋的看到了if

def initialize(name, filter, kind, options, chain_config)
  @chain_config  = chain_config
    @name    = name
  @kind    = kind
@filter  = filter
    @key     = compute_identifier filter
  @if      = Array(options[:if]) # 这个!!这个!!
  @unless  = Array(options[:unless])
end

接下来的问题,其实就比较轻松了,这和Callback被执行时的,@if 是如何处理的。

在当前的页面,还能看到这段代码

def conditions_lambdas
    @if.map { |c| CallTemplate.build(c, self).make_lambda } +
      @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
end

这里,整个 @if Array 转化成了一个由 CallTemplate.build(c, self).make_lambda 组成的Array,里面是什么呢?是lambda

接着就是要搞这个 conditions_lambdas返回的 Array 去了哪里。

我们还是在同一个文件里code

def apply(callback_sequence)
  user_conditions = conditions_lambdas
  user_callback = CallTemplate.build(@filter, self)

  case kind
  when :before
    Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
  when :after
    Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
  when :around
    callback_sequence.around(user_callback, user_conditions)
  end
end

这里其实有涉及到了,ActiveSupport::Callbacks的执行逻辑,就不再深入,直接跳到最终执行的地方,还是同一个文件code

def skip?(arg)
  arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
end

这里有!@user_conditions.all? { |c| c.call(arg.target, arg.value) } 因为这是一个 Array

查看 Enumerable .all?的定义。code

all? [{ |obj| block } ] → true or false
Passes each element of the collection to the given block. The method returns true if the block never returns false or nil. If the block is not given, Ruby adds an implicit block of { |obj| obj } which will cause all? to return true when none of the collection members are false or nil.

所以,if options 实际上是在这里执行的。

@tesla_lee 实在是太感谢了, 解释的太详细了。 然后 能再问一个问题吗?

set_callback(:validate, *args, &block) 这里 设置了 一个 callback. 但是 这个 callback 是在哪一步执行的呢? 因为我看到 还有 before_validate, after_validate 这两个方法,也会设置 callback。

所以现在这个 set_callback(:validate, *args, &block) ,看样子有点像 在 调用 validate 之前的 会调用。。。
不是很确定。

以前为了学习Callbacks参照它的逻辑写了一个迷你版的 demo

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