ActiveRecord::Enumという機能がRails4.1から入りました。
気軽に使えるので、割りと積極的に使っているのですがその中である問題が発生しました。
ActiveRecord::Enumの提供してくれる標準で提供してくれるインターフェイスだけでは*1、whereの引数で指定する値は実際にSQLのWHERE句に指定されるものが渡すため本来は文字列が使えないが、何となく使えそうなので渡してみると、値が0として動作してしまう、という問題です
以下、例です。NotifyEndpointというPush通知の送り先を永続化するテーブルについて考えた場合、platformとしてiPhone用のAPNSとAndroid用のGCMなどのプラットフォームをenumで指定してみます。
class NotifyEndpoint
enum platform: [:apns, :gcm]
end
# OK: newやcreateでは渡せる
endpoint = NotifyEndpoint.new(platform: :apns)
# OK: platform=メソッドにも渡せる
endpoint.platform = :gcm
# OK: apnsというスコープは定義されている
endpoints = NotifyEndpoint.apns
# NG: whereに文字列やシンボルを渡しても正しく動作しない
# しかしエラーが出ずに0としてSELECTされる!!
endpoints = NotifyEndpoint.where(platform: :gcm)
#=> SELECT * FROM notify_endoints WHERE platform = 0;
# OK: NotifyEndpoint.platformsから、enumの序数を取得してwhereで指定する
endpoints = NotifyEndpoint.where(platform: NotifyEndpoint.platforms[:gcm])
#=> SELECT * FROM notify_endoints WHERE platform = 1;
というように、基本的には文字列シンボルでenumを扱うことが出来るんですが、標準で定義されるスコープ以外で、SELECTしたいときは要注意です。whereにはenumの要素の名前を受け付けてくれないのですがエラーが出ないのでやっかいです。正常ケースのテストが1個目の要素しかテストしてないと普通に通るのでうっかりバグを入れてしまいます
かと言って、毎回NotifyEndpoint.where(platform: NotifyEndpoint.platforms[:gcm])
のような記述方法をするのはクラス名2回書かなきゃならないし、enumの序数についていちいち意識しなきゃいけないのは、面倒くさいです。
↑の問題を回避しつつ、面倒くさくない記法が出来るようenum名でscopeを生やしてくれるパッチを書きました lib/active_record/enum/scoping.rb
などに置いてconfig/initializers/*.rbのどこかでrequireしておけば使えるかと思います。
if defined?(ActiveRecord) && defined?(::ActiveRecord::Enum)
module ActiveRecord
module Enum
module Scoping
def self.extended(base)
::ActiveRecord::Enum.alias_method_chain(:enum, :scoping)
end
end
def enum_with_scoping(definitions)
enum_without_scoping(definitions)
define_scoping_method(definitions)
end
private
def define_scoping_method(definitions)
definitions.each do |name, values|
scoping_method_name = "#{name}_as".to_sym
scope(scoping_method_name, - > (item) { enum_scope(name, item) }})
end
end
end
def enum_scope(enum_name, item_name)
query_hash = {}
query_hash[enum_name.to_sym] = send(enum_name.to_s.pluralize)[item_name]
where(query_hash)
end
end
::ActiveRecord::Base.extend(::ActiveRecord::Enum::Scoping)
end
これを導入すると以下のように、<enum_name>_as
というスコープが定義されるので、文字列・シンボルを渡しても動作するようになりましたし、いちいちクラスメソッドで値を変換したり、自前で毎回スコープを定義したりせず済むようになりました
endpoints = NotifyEndpoint.platform_as(:gcm)
#=> SELECT * FROM notify_endpoints WHERE platform = 1;
需要があればgemにしてみます。
- *1(実装はそんなに長くないので読んでみることをおすすめします
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/enum.rb)