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


完整调用栈如下

20170702_121828_946_enum.html

其脉络基本上就在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>
'


调用栈如下

20170702_155707_889_enum_writer.html

调用的是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


调用栈如下

20170702_161326_452_enum_save.html

从图中可搜到EnumType#cast,如下