Rails ActiveModel
AttributeAssignment Callbacks Dirty Validations
rails activemodel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module ActiveModel
extend ActiveSupport::Autoload
autoload :AttributeAssignment
autoload :AttributeMethods
autoload :BlockValidator, 'active_model/validator'
autoload :Callbacks
autoload :Conversion
autoload :Dirty
autoload :EachValidator, 'active_model/validator'
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
autoload :Name, 'active_model/naming'
autoload :Naming
autoload :SecurePassword
autoload :Serialization
autoload :TestCase
autoload :Translation
autoload :Validations
autoload :Validator
eager_autoload do
autoload :Errors
autoload :RangeError, 'active_model/errors'
autoload :StrictValidationFailed, 'active_model/errors'
autoload :UnknownAttributeError, 'active_model/errors'
end
end
attribute_assignment
让任何的ruby对象接受hash赋值,实现是动态的分发, 2. 防止 = ActionController::Parameters 没有调用permit函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.nil? || new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
private
def _assign_attributes(attributes)
attributes.each do |k, v|
_assign_attribute(k, v)
end
end
def _assign_attribute(k, v)
if respond_to?("#{k}=")
public_send("#{k}=", v)
else
raise UnknownAttributeError.new(self, k)
end
end
attribute_methods
源码 目的 在方法中添加前缀后缀等(好像没啥用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
module AttributeMethods extend ActiveSupport::Concern NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false self.attribute_aliases = {} self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end module ClassMethods def attribute_method_suffix(*suffixes) self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix } end def generated_attribute_methods #:nodoc: @generated_attribute_methods ||= Module.new { extend Mutex_m }.tap { |mod| include mod } end def define_attribute_method(attr_name) attribute_method_matchers.each do |matcher| method_name = matcher.method_name(attr_name) unless instance_method_already_implemented?(method_name) define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: defn = if name =~ NAME_COMPILABLE_REGEXP "def #{name}(*args)" else "define_method(:'#{name}') do |*args|" end extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) target = if send =~ CALL_COMPILABLE_REGEXP "#{"self." unless include_private}#{send}(#{extra})" else "send(:'#{send}', #{extra})" end mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 #{defn} #{target} end RUBY end class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '') @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" end def match(method_name) if @regex =~ method_name AttributeMethodMatch.new(method_missing_target, $1, method_name) end end def method_name(attr_name) @method_name % attr_name end def plain? prefix.empty? && suffix.empty? end end end
调用栈为:
1 2 3 4 5 6 7 8 9
attribute_method_affix prefix: 'reset_' define_attribute_methods :name |- attribute_method_affix 的调用产生 一个 AttributeMethodMatcher的数组,维系着 添加前缀后缀的标记 |- define_attribute_methods |- attribute_method_matchers.each |matcher| |- define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s define_attribute_methods 作用的symbol, 将前缀后缀的标记应用在参数上, 主动定义 prefix_name_sufix 的方法,并将方法的调用分发到 prefix_attribute_sufix函数中.值的注意的地方
1 2 3 4 5 6 7 8 9 10 11
# @method_name = "prefix_%s", method_name(name) => prefix_name def method_name(attr_name) @method_name % attr_name end # Module.new 产生新的module 并在当前类中include, 之后 define_proxy_call等都是在这个 module 中进行操作, 包括去除方法等, 保证了,不影响类中的其他代码 def generated_attribute_methods #:nodoc: @generated_attribute_methods ||= Module.new { extend Mutex_m }.tap { |mod| include mod } end
callbacks
代码调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#activesupport class Record include ActiveSupport::Callbacks define_callbacks :save set_callback :save, :before, :saving_message def saving_message puts "saving..." end set_callback :save, :after do |object| puts "saved" end def save run_callbacks :save do puts "- save" end end end person = PersonRecord.new person.save
调用栈分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# /activesupport/callbacks |- define_callbacks(names) |- class_attribute "_#{name}_callbacks", instance_writer: false |- set_callbacks name, CallbackChain.new(name, options) |- set_callback :save, :before, :saving_message |- callback_chain = get_callbacks(name) |- Callback.build(callback_chain, filter, type, options) |- set_callbacks name, callback_chain |- run_callbacks :save |- __run_callbacks__(callbacks, &block) |- runner = callbacks.compile |- CallbackChain#compile |- final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } |- @chain.reverse.inject(final_sequence) do |callback_sequence, callback| |- callback.apply callback_sequence |- end |- runner.call(Filters::Environment.new(self, false, nil block)).value |- e = Filters::Environment.new(self, false, nil, block) |- runner.call(e).value- 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
def define_callbacks(*names) options = names.extract_options! names.each do |name| class_attribute "_#{name}_callbacks", instance_writer: false set_callbacks name, CallbackChain.new(name, options) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) __run_callbacks__(_#{name}_callbacks, &block) end RUBY end end def set_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) self_chain = get_callbacks name mapped = filters.map do |filter| Callback.build(self_chain, filter, type, options) end __update_callbacks(name) do |target, chain| options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) target.set_callbacks name, chain end end def __run_callbacks__(callbacks, &block) if callbacks.empty? yield if block_given? else runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) runner.call(e).value end end class Callback #:nodoc:# def self.build(chain, filter, kind, options) new chain.name, filter, kind, options, chain.config end attr_accessor :kind, :name attr_reader :chain_config 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 def compile @callbacks || @mutex.synchronize do final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| callback.apply callback_sequence end end end def apply(callback_sequence) user_conditions = conditions_lambdas user_callback = make_lambda @filter case kind when :before Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter) when :after Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config) when :around Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config) end end end class CallbackSequence def initialize(&call) @call = call @before = [] @after = [] end def before(&before) @before.unshift(before) self end def after(&after) @after.push(after) self end def around(&around) CallbackSequence.new do |arg| around.call(arg) { self.call(arg) } end end def call(arg) @before.each { |b| b.call(arg) } value = @call.call(arg) @after.each { |a| a.call(arg) } value end end class Before def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter) halted_lambda = chain_config[:terminator] if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) else halting(callback_sequence, user_callback, halted_lambda, filter) end end def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) callback_sequence.before do |env| target = env.target value = env.value halted = env.halted if !halted && user_conditions.all? { |c| c.call(target, value) } result_lambda = -> { user_callback.call target, value } env.halted = halted_lambda.call(target, result_lambda) if env.halted target.send :halted_callback_hook, filter end end env end end private_class_method :halting_and_conditional def self.halting(callback_sequence, user_callback, halted_lambda, filter) callback_sequence.before do |env| target = env.target value = env.value halted = env.halted unless halted result_lambda = -> { user_callback.call target, value } env.halted = halted_lambda.call(target, result_lambda) if env.halted target.send :halted_callback_hook, filter end end env end end private_class_method :halting end
大概的调用 调用顺序为, define_callbacks :save 定义callback的名字,初始化CallbackChain, set_callback :save, :before, :save_messsage, 在CallbackChain中添加Callback 的实例, 在最后run_callback 的时候最为重要, 将前面两步做的操作都进行了兑现, 1. 调用CallBackChain#compile, compile 有意思的是初始化了一个CallbackSequence实例, 并对Callback数组进行inject Callback.apply 中最为重要, CallBack.apply 中将 Filters::Before, Filters::After中的其他通用部分, 执行条件(set_callback :save, :before, :do_something if: :xxx?), hook函数(do_something) 进行了lambda化,(条件为conditions_lambdas函数, hook为make_lambda), 并根据 kind(:before, :after, :around)进行了分发, 下降到Filters::Before 中, 则在 hook 的lambda的基础上进行了, 完善,完成了各自 Before, After 等的具体操作,这是最底层的具体到执行逻辑的部分。2. 执行, 上面的lambda化结果, 提供了一个Environment的环境, 包含:halt, :value, :target 等信息,
active_model 中的 callback
active_model 中封装了, activesupport 中设定callback的繁琐步骤,而是通过 define_model_callbacks 来设定
1 2 3 4 5 6 7
|- define_model_callbacks :save |- define_callbacks(callback, options) |- send("_define_#{type}_model_callback", self, callback) |- klass.define_singleton_method("before_#{callback}") do |*args, &block| |- set_callback(:"#{callback}", :before, *args, &block) |- end1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
module ActiveModel module Callbacks def self.extended(base) #:nodoc: base.class_eval do include ActiveSupport::Callbacks end end def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] }.merge!(options) types = Array(options.delete(:only)) callbacks.each do |callback| define_callbacks(callback, options) types.each do |type| send("_define_#{type}_model_callback", self, callback) end end end private def _define_before_model_callback(klass, callback) #:nodoc: klass.define_singleton_method("before_#{callback}") do |*args, &block| set_callback(:"#{callback}", :before, *args, &block) end end def _define_around_model_callback(klass, callback) #:nodoc: klass.define_singleton_method("around_#{callback}") do |*args, &block| set_callback(:"#{callback}", :around, *args, &block) end end def _define_after_model_callback(klass, callback) #:nodoc: klass.define_singleton_method("after_#{callback}") do |*args, &block| options = args.extract_options! options[:prepend] = true conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| v != false } options[:if] = Array(options[:if]) << conditional set_callback(:"#{callback}", :after, *(args << options), &block) end end end end
good-part
代码中使用了,大量的lambda化最为震撼(其中包括 条件执行,hook函数), 将符号配置, 执行条件配置都lambda化, 变成了一个可以随时执行的代码(compile 的含义很符合), 存储在一个数组中,hook的执行 变成了执行数组的 each item.call, 其中个个callback如何确定环境(比如是否执行,是否已经callback中断了)通过Environment 对象传递
- 问题 问题为什么需要CallbackChain 到CallbackSequence的转换呢?
- CallbackChain中主要针对Callback的实例数组操作, delete, append, prepend, clear, compile 等, CallbackSequence 也是类似的,存储lambda之后的可执行数组
dirty
dirty 根据文档的描述是,为了提供一个工具集来跟踪变量变化(之前以为是auto, 用了一些callback, 或者元编程之类的结果不是!!!, 而是提供了一个工具集, 在使用中, 需要手动调用方法)
下面的可见一斑:
- 在使用的类中 include ActiveModel::Dirty
- 使用define_attribute_methods 设定跟踪的变量
- 在变量变化之前 调用 [attr_name_]will_change!, 例如name, 在name=()中调用name_will_change!
- 调用函数 changes_applied 来”持久化”, 将现有的改动, 复制到之前(previous)中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
include ActiveModel::AttributeMethods
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
attribute_method_suffix '_previously_changed?', '_previous_change'
attribute_method_affix prefix: 'restore_', suffix: '!'
end
def attribute_will_change!(attr)
return if attribute_changed?(attr)
begin
value = __send__(attr)
value = value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
end
set_attribute_was(attr, value)
end
def changed_attributes
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
end
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
end
# This is necessary because `changed_attributes` might be overridden in
# other implementations (e.g. in `ActiveRecord`)
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
def changes_applied # :doc:
@previously_changed = changes
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
validations
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Person include ActiveModel::Validations validates_with MyValidator end class MyValidator < ActiveModel::Validator def validate(record) if some_complex_logic record.errors.add(:base, "This record is invalid") end end private def some_complex_logic # ... end end
调用栈分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
# validation 中的代码 define_callbacks :validate, scope: :name class_attribute :_validators, instance_writer: false |- validates_with MyValidator |- options = args.extract_options! |- validator = klass.new(options, &block) |- validator.attributes.each {|attribute| _validators[attribute.to_sym] << validator } |- validate(validator, options) |- set_callback(:validate, *args, &block) # 一些内置的validator, 例如PresenceValidator |- validates_presence_of(*attr_names) |- validates_with PresenceValidator, _merge_attributes(attr_names) |- valid?(context = nil) |- run_validations! |- _run_validate_callbacks ....... |- validate(record) |- validate_each(record, attribute, value)一个主要的数据流走向为:
在include Validator 时候, define_callbacks :validate, scope: :name。 设定callback, scope: :name 的作用为调用 一个instance callback时候调用的方法, 在 define_callbacks :save, MyValidator, :before, scope: [:kind, :name], 在执行callback时候调用 MyValidator#before_save, Callback#apply 中存在
validates_with Class, options, 构造实例, 调用validate, 调用set_callback设定 callback时候的lambda, 2. 存在一些内置的校验器, validates_presence_of :name, 通过统一的调用 validate_with Class, options
valid?, 执行callback 中的lambda校验, 其中有意思的是valid?(context = nil), valid?的调用存在一个 context参数, 可以传递 在 validate_with Class, on: :save,的参数, 来决定执行特定环境中的validate, 所以猜想贯穿到 ActiveRecord#.save 时候的调用,应该存在类似的 valid?(:save)代码逻辑, 执行个个validator实例中的validate函数, 通过 继承EachValidator 分发到validate_each
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
class EachValidator < Validator #:nodoc: attr_reader :attributes def initialize(options) @attributes = Array(options.delete(:attributes)) raise ArgumentError, ":attributes cannot be blank" if @attributes.empty? super end end # active_model/validation.rb module ActiveModel module Validations extend ActiveSupport::Concern included do extend ActiveModel::Callbacks extend HelperMethods include HelperMethods attr_accessor :validation_context private :validation_context= define_callbacks :validate, scope: :name class_attribute :_validators, instance_writer: false self._validators = Hash.new { |h,k| h[k] = [] } end module ClassMethods def validate(*args, &block) options = args.extract_options! 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 args << options set_callback(:validate, *args, &block) end end end end # active_model/validations/with.rb module ActiveModel module Validations module ClassMethods def validates_with(*args, &block) options = args.extract_options! options[:class] = self args.each do |klass| validator = klass.new(options, &block) if validator.respond_to?(:attributes) && !validator.attributes.empty? validator.attributes.each do |attribute| _validators[attribute.to_sym] << validator end else _validators[nil] << validator end validate(validator, options) end end end end end module ActiveModel module Validations class PresenceValidator < EachValidator # :nodoc: def validate_each(record, attr_name, value) record.errors.add(attr_name, :blank, options) if value.blank? end end module HelperMethods def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end end end end class EachValidator < Validator #:nodoc: attr_reader :attributes def initialize(options) @attributes = Array(options.delete(:attributes)) raise ArgumentError, ":attributes cannot be blank" if @attributes.empty? super check_validity! end def validate(record) attributes.each do |attribute| value = record.read_attribute_for_validation(attribute) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) validate_each(record, attribute, value) end end end