active_record的enum类方法(还可以new)
One of the newest additions to Active Record introduced in Rails 4.1 is the ability to set an attribute as an enumerable. Once an attribute has been set as an enumerable, Active Record will restrict the assignment of the attribute to a collection of predefined values
跟踪类方法enum到底干什么
class Student < ApplicationRecord
binding.trace_tree(htmp: 'rails/enum') do
enum gender: [:male, :female]
end
end
完整调用栈如下
其脉络基本上就在enum一个方法中
# activerecord-5.0.4/lib/active_record/enum.rb
def enum(definitions)
klass = self
enum_prefix = definitions.delete(:_prefix)
enum_suffix = definitions.delete(:_suffix)
definitions.each do |name, values|
# 枚举值及其对应的integer
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym
# 可以通过“attr的复数”方法获取该attr的枚举值及对应integer
# def self.statuses() statuses end
detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
detect_enum_conflict!(name, name)
detect_enum_conflict!(name, "#{name}=")
# 给这个attr关联一个EnumType
# 以便赋值时能检查值是否在枚举值中
# 在的话,就转换成integer,后面再分析
attr = attribute_alias?(name) ? attribute_alias(name) : name
decorate_attribute_type(attr, :enum) do |subtype|
EnumType.new(attr, enum_values, subtype)
end
# 便利方法都定义到单独一个module中
_enum_methods_module.module_eval do
# 除了integer,对应值也可以用hash来自定义的
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
# prefix和suffix可以是用attr名或自定义,当让不假pre、suf也可以
if enum_prefix == true
prefix = "#{name}_"
elsif enum_prefix
prefix = "#{enum_prefix}_"
end
if enum_suffix == true
suffix = "_#{name}"
elsif enum_suffix
suffix = "_#{enum_suffix}"
end
value_method_name = "#{prefix}#{value}#{suffix}"
enum_values[value] = i
# 定义便利方法:#active?,#active!,.active
# def active?() status == 0 end
klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
define_method("#{value_method_name}?") { self[attr] == value.to_s }
# def active!() update! status: :active end
klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
define_method("#{value_method_name}!") { update!(attr => value) }
# scope :active, -> { where status: 0 }
klass.send(:detect_enum_conflict!, name, value_method_name, true)
klass.scope value_method_name, -> { where(attr => value) }
end
end
# 可以通过类方法defined_enums获取所有枚举字段的枚举值及对应integer
defined_enums[name.to_s] = enum_values
end
end
转换到数据库值
调用某个attr的writer方法时,其实是会做type cast的。因为刚才gender字段以关联了一个EnumType,所以type cast就由EnumType来做
如果赋的值不在枚举值中,是会报错的
irb(main):003:0> binding.trace_tree(htmp: 'rails/enum_writer'){s.gender = 'fdf'} ArgumentError: 'fdf' is not a valid gender from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/enum.rb:139:in `assert_valid_value' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/attribute.rb:67:in `with_value_from_user' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/attribute_set.rb:53:in `write_from_user' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/attribute_methods/write.rb:50:in `write_attribute_with_type_cast' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/attribute_methods/write.rb:32:in `write_attribute' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.0.4/lib/active_record/attribute_methods/write.rb:20:in `__temp__7656e6465627=' from (irb):3:in `block in irb_binding' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/trace_tree-0.2.9/lib/trace_tree.rb:47:in `instance_eval' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/trace_tree-0.2.9/lib/trace_tree.rb:47:in `generate' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/trace_tree-0.2.9/lib/trace_tree.rb:13:in `trace_tree' from (irb):3 from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.0.4/lib/rails/commands/console.rb:65:in `start' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.0.4/lib/rails/commands/console_helper.rb:9:in `start' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.0.4/lib/rails/commands/commands_tasks.rb:78:in `console' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.0.4/lib/rails/commands/commands_tasks.rb:49:in `run_command!' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.0.4/lib/rails/commands.rb:18:in `<top (required)="">' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:293:in `require' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:293:in `block in require' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:259:in `load_dependency' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:293:in `require' from /home/z/test_rails/school/bin/rails:9:in `<top (required)="">' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:287:in `load' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:287:in `block in load' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:259:in `load_dependency' from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activesupport-5.0.4/lib/active_support/dependencies.rb:287:in `load' from /home/z/.rbenv/versions/2.4.0/lib/ruby/2.4.0/rubygems/core_ext/kernel_require.rb:55:in `require' from /home/z/.rbenv/versions/2.4.0/lib/ruby/2.4.0/rubygems/core_ext/kernel_require.rb:55:in `require' from -e:1:in `</top></top>
'
调用栈如下
调用的是EnumType#assert_valid_value
# activerecord-5.0.4/lib/active_record/enum.rb
class EnumType < Type::Value
def initialize(name, mapping, subtype)
@name = name
@mapping = mapping
@subtype = subtype
end
def cast(value)
return if value.blank?
if mapping.has_key?(value)
value.to_s
elsif mapping.has_value?(value)
mapping.key(value)
else
assert_valid_value(value)
end
end
def assert_valid_value(value)
unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
raise ArgumentError, "'#{value}' is not a valid #{name}"
end
end
end
而如果值在枚举值中,就会很顺利的通过assert_valid_value,然后在save时进行type cast,取出对应的integer
irb(main):006:0> binding.trace_tree(htmp: 'rails/enum_save'){s.save}
(16.1ms) begin transaction
SQL (51.2ms) UPDATE "students" SET "updated_at" = ?, "gender" = ? WHERE "students"."no" = ? [["updated_at", "2017-07-02 08:13:26.673089"], ["gender", 0], ["no", "1"]]
(31.5ms) commit transaction
=> true
调用栈如下
从图中可搜到EnumType#cast,如下
