Railsを使っていて何かと便利なのが「enum」です。
他の言語では列挙型(Enumerable)と呼ばれることがありますが、実態としてはActiveRecordに定義されたenumという関数です。modelファイルの中でカラム値のバリエーションを指定することが出来るのが非常に便利です。
# Sushiモデルのkindにはenumに定義した値しか登録できない class Sushi enum kind: { nigiri: 0, maki: 1, gunkan: 2, other: 99 } validates :kind, inclusion: { in: Sushi.kinds.keys } end
しかし、データベース上ではenum型にはならずINTEGERとして登録されます。個人的にはSMALLINTでもいいんじゃないかと思います。
さらにenumを定義するといくつかのメソッドが自動で定義されます。個人的にはレコードのカラム値が指定値どうか判定できるメソッド(eg: nigiri?)が便利でよく使っています。
# enum情報の取得 Sushi.kinds # { nigiri: 0, maki: 1, gunkan: 2, other: 99 } # Sushiクラスのインスタンスのkindが0(nigiri)かどうか Sushi.new(kind: 0).nigiri? # true # Sushiクラスのインスタンスのkindを1(maki)に更新する Sushi.new(kind: 0).maki! # # Sushiクラスに対応するテーブルからkindが0(nigiri)の一覧を取得 ≒ Sushi.where(kind: 0) Sushi.nigiri
今回は便利なenumがどのようにして作られているのか実際のコードを見ながら、自分で実装してみることで理解を深めていきます。
enumのコード
まずはenumのコードを見てみます。先ほど書いたようにenumは関数として定義されています。
(※見やすさのため一部を省略しています。全体はURLよりご確認ください)
def enum(definitions) klass = self enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new # 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 } : _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index : value_method_name = "#{prefix}#{value}#{suffix}" enum_values[value] = i # def active?() status == 0 end define_method("#{value_method_name}?") { self[attr] == value.to_s } # def active!() update! status: :active end define_method("#{value_method_name}!") { update!(attr => value) } # scope :active, -> { where status: 0 } klass.scope value_method_name, -> { where(attr => value) } end end defined_enums[name.to_s] = enum_values end end
rails/enum.rb at 21e5fd4a2a1c162ad33708d3e01b1fda165f204d · rails/rails · GitHub
順に処理を見てみます。まずは引数のdefinitions
ですが、enum関数の実行時を見て分かるようにハッシュが束縛されています。
# 括弧が省略されているだけで enum({ kind: { nigiri: 0 ...}})と同じ enum kind: { nigiri: 0, maki: 1, gunkan: 2, other: 99 }
関数の冒頭では_prefix
と_suffix
がハッシュのキーに指定されていればハッシュから削除して、バリューを変数に束縛しています。どちらもenumに設定可能なオプションのようです。コードを見るまでenumにオプションがあることを全く知らなかったので、こうやってコードを見てみると新たな発見があるものです。
参考: Rails5 から enum 使う時は_prefix(接頭辞)_suffix(接尾辞)を使おう - Qiita
次にdefinitions
に対してeach
を実行していきます。ここからが各種メソッドを定義している部分です。
self.statuses()
enumが定義されたクラスのsingleton_classに対してdefine_method関数を使用して関数を定義しています。pluralize
は単語を複数形にするRailsが提供するメソッドです。
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
この処理に実際に値が渡されると以下のようになります。現時点ではenum_values
はActiveSupport::HashWithIndifferentAccess
のインスタンスが返るだけですが、後の処理でenum_values
のキーバリューにしっかりと値を登録している(enum_values[value] = i
)ので、問題なさそうです。
Sushi.singleton_class.send(:define_method, :kind.to_s.pluralize) { enum_values }
kinds関数が返すのはActiveSupport::HashWithIndifferentAccess
のインスタンス。
Sushi.kinds.class => ActiveSupport::HashWithIndifferentAccess ActiveSupport::HashWithIndifferentAccess.new({ nigiri: 0, maki: 1, gunkan: 2, other: 99 }).class => ActiveSupport::HashWithIndifferentAccess
active?()
同じようにdefine_method関数を呼び出していますが、こちらはsingleton_classへの定義ではないため、この関数はインスタンスメソッドになります。
define_method("#{value_method_name}?") { self[attr] == value.to_s }
例えばnigiri?
が呼び出された時にはインスタンスのkind属性がnigiri
と一致するかを判定する処理が実行されます。
# self[attr] == value.to_s self[kind] == 'nigiri'
active!()とscope :active
どちらもほぼ同じです。
active!
ではupdate!メソッドの呼び出し、scope :active
ではスコープの作成を行なっています。
# active! define_method("#{value_method_name}!") { update!(attr => value) } # scope :active klass.scope value_method_name, -> { where(attr => value) }
今回はすでに定義されているenumなのかなどの重複判定処理は簡単のため省略しましたが、やっていることはそんなに難しくなさそうです。これならオリジナルのenumが作れそうなので実際にオレオレenumを作ってみましょう。
オレオレenumの作成
まずはenum関数をモジュールに定義します。この時点ではただmoduleに定義されたenum関数です。
module MyEnum def enum(definitions) end end
self.statuses()の実装
まずはself.statuses()
の実装を行います。
definitions
をeach_pair
メソッドを用いてキーバリューのペアを順に処理していきます。このMyEnumモジュールを使用したいクラスでextendして使ってもらうことを想定してself.singleton_class
に対してdefine_method関数を呼び出します。ただ、Railsが提供するpluralize
メソッドは使えないため、今回はカラム名 + _hash
の形式で呼び出せるようにします。
module MyEnum def enum(definitions) definitions.each_pair do |name, values| self.singleton_class.define_method("#{name}_hash") { values } end end end
試しに適当なクラスを作成してextendして呼び出してみます。
class Sushi extend MyEnum enum kind: { nigiri: 0, maki: 1, gunkan: 2, other: 99 } end puts Sushi.kind_hash # {:nigiri=>0, :maki=>1, :gunkan=>2, :other=>99}
無事にハッシュの取得ができました。
active?()の実装
代表してこのメソッドを作成します。同じようにdefine_method関数を呼び出しますが、こちらはインスタンスメソッドではないので、そのままdefine_method関数を呼び出します。この実装では制約としてextendしたクラス先でenumの引数に渡されるハッシュのキーとインスタンス変数の名前が一致する必要があります。
module MyEnum def enum(definitions) definitions.each_pair do |name, values| self.singleton_class.define_method("#{name}_hash") { values } values.each_pair do |key, value| instance_var_name = "@#{name}".to_sym define_method("#{key}?") { self.instance_variable_get(instance_var_name) == value } end end end end
動作を見てみましょう。確認のためにインスタンス変数とattr_readerを設定しています。
class Sushi extend MyEnum enum kind: { nigiri: 0, maki: 1, gunkan: 2, other: 99 } attr_reader :kind def initialize(kind:) @kind = kind end end s = Sushi.new(kind: 0) puts s.nigiri? # true
良い感じです。複数のインスタンス変数を定義しても上手く動きました。
class Sushi extend MyEnum enum kind: { nigiri: 0, maki: 1, gunkan: 2, other: 99 } enum wasabi: { no_wasabi: 0, in_wasabi: 1 } attr_reader :kind, :wasabi def initialize(kind:, wasabi:) @kind = kind @wasabi = wasabi end end s = Sushi.new(kind: 0, wasabi: 1) puts s.nigiri? # true puts s.in_wasabi? # true
最後に
今回はRailsに定義された便利なenum関数について処理を追ってみました。
内容を省いた箇所はあるものの、蓋を開けてみるとそれほど難しいことはしておらずdefine_method関数を上手く利用していることが分かりました。MyEnumの作成にも成功しました。
過去にもdefine_methodが登場するコードを読んでいるのでぜひ、こちらもご覧ください。
少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。