enum in rails 4

作者:周星 发布:2015-08-29

Rails 4.1 开始提供了 enum 方法,enum 方法声明了 enum 属性使得 enum 的值 map 到数据库中的数字,看下面这个例子:

class Blog < ActiveRecord::Base
  enum status: [ :published, :hide ]
end

# blog.update! status: 0
blog.published!
blog.published? # => true
blog.status  # => "published"

# blog.update! status: 1
blog.hide!
blog.hide? # => true
blog.status    # => "hide"

# blog.update! status: 1
blog.status = "hide"

# blog.update! status: nil
blog.status = nil
blog.status.nil? # => true
blog.status      # => nil

从上面的例子中我们可以看到,enum 方法接收一个 hash( {status: [:published, :hide]} ) 作为参数,这个 hash 的 key(status) 为对应的数据库中的字段名,value 为一个 symbol 数组([:published, :hide]),数组的索引为数据库中的值,然后根据数组中元素名字自动生成两个方法,分别为以感叹号结尾(update)和以问号结尾(返回 true or false)。所以在数据库中如果 blog 的 status 为 0,则 blog 就是 published 的,如果 status 为 1,则 blog 就是 hide 的。

同时这里有一个最佳实践,就是在建表的时候设置 hash value 的第一个值为 default 值:

create_table :blogs do |t|
  t.column :status, :integer, default: 0
end

enum 还会根据 hash value 生成两个 scope,分别为 published 和 hide:

Blog.published.count
Blog.hide.count

如果 status 的值同样只有两种,分别对应 published 和 hide,但是这两个值不是 0 和 1 怎么办?enum 参数 hash value 可以是一个 hash,看下面这个例子:

class Blog < ActiveRecord::Base
  enum status: { publish: 0, hide: 1 }
end

注意:因为数组是有序的,所以如果想新增一个发布状态(status),则必须增加在尾部,否则会打乱数组中元素的位置,如果想删除一个发布状态,则应该修改 status 对应的值为 hash 了。

在一些情况下,你可能想直接访问这个 mapping,这个 mapping 通过一个类方法暴露给外部,而这个类方法的名字正是 enum 参数 hash key 的复数:

Blog.statuses # => { "published" => 0, "hide" => 1 }

这样你就可以取到对应的值来做比较或者查询:

Blog.where("status <> ?", Blog.statuses[:hide])

有些时候你可能想拿到其对应的Integer value,可以通过这种形式来获取:

blog[:status]
# 0 or 1

从 enum 这个功能我们就可以知道,它的源码应该不复杂,主要就是用元编程生成一些方法:

# activerecord/lib/active_record/enum.rb

def enum(definitions)
  klass = self
  definitions.each do |name, values|
    # statuses = { }
    enum_values = ActiveSupport::HashWithIndifferentAccess.new
    name        = name.to_sym

    # 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}=")

    attribute name, EnumType.new(name, enum_values)

    _enum_methods_module.module_eval do
      pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
      pairs.each do |value, i|
        enum_values[value] = i

        # def active?() status == 0 end
        klass.send(:detect_enum_conflict!, name, "#{value}?")
        define_method("#{value}?") { self[name] == value.to_s }

        # def active!() update! status: :active end
        klass.send(:detect_enum_conflict!, name, "#{value}!")
        define_method("#{value}!") { update! name => value }

        # scope :active, -> { where status: 0 }
        klass.send(:detect_enum_conflict!, name, value, true)
        klass.scope value, -> { klass.where name => value }
      end
    end
    defined_enums[name.to_s] = enum_values
  end
end

每次定义方法前先去检测即将生成的方法名,如果通过检测,则定义方法,否则抛出异常。

支付宝扫码赞助博主


评论(0)