初投稿の@sunflatです。好きなプログラミング言語は MSX-BASIC です。
Rails 4.1でActiveRecordに追加された enum について、ちょっと調べてみました。
本当は、Misocaの開発でenumを使ってその実例を紹介する予定だったのですが、後述する理由により今回は適していなかったので使いませんでした。
そこで今回は、(株)Misocaの近くにある「ムガルパレス」というインドカレー屋さんの、ランチセットのメニューを例に、enumを使う時に気をつけたいポイントをいくつか紹介します。
enum の使い方
ActiveRecordの enum を使うと、プログラムからは文字列(名前)でアクセスでき、DBには整数値で保存される属性を作成できます。
さっそくランチセットのモデルを作成しましょう。カレーの種類として、チキン、ポーク、野菜、豆 を選択できるとします。enum を使うためには、属性にセットできる文字列(名前)を予め列挙しておきます。
class LunchSet < ActiveRecord::Base enum curry: [:chicken, :pork, :vegitables, :beans] end
class CreateLunchSets < ActiveRecord::Migration def change create_table :lunch_sets do |t| t.integer :curry t.timestamps null: false end end end
属性(curry)を読み書きする時に、名前の文字列(chicken、porkなど)を使うことができます。
また、DBに保存する時は整数値として保存されます。名前に対応する整数値は、名前リストの先頭から順に、0,1,2,... となります。
$ rails console > lunch = LunchSet.create(curry: 'pork') => #<LunchSet ..., curry: 1, ...> > lunch.curry => "pork" > lunch['curry'] # DBに保存される値 => 1 > lunch.curry = 'beans' > lunch['curry'] # DBに保存される値 => 3 > lunch.curry = 'hoge' # 存在しない名前をセットすると、例外発生 ArgumentError: 'hoge' is not a valid curry
他にも、名前?
メソッド(現在その値がセットされているかどうかを返す)、名前!
メソッド(その値をセットしてsave!する)などが定義されます。
> lunch.beans? => true > lunch.chicken! > lunch.curry => "chicken"
対応する整数値を明示する方法
ここで、ポークカレーが廃止されて、新たにシュリンプカレーが追加されたとしましょう。(ムガルパレスではどちらも注文できるので安心して下さい)
単純に、名前リストから:pork
を削除すると、vegitablesとbeansに対応する整数値が変わってしまうので、このような場合はHashを使って対応する整数値を明示します。
個人的には、単純な連番の場合でも、最初から対応する整数値を明示しておいた方が分かりやすい気がします。
class LunchSet < ActiveRecord::Base enum curry: { chicken: 0, vegitables: 2, beans: 3, shrimp: 4 } end
ちなみに、修正前に lunch.curry == 'pork'
となっていた行がDBに残っている場合、修正後にそれを読み込むと lunch.curry == nil
になります。
複数のenum属性を持つ場合の注意
せっかくのランチセットなので、飲み物も追加しましょう。
飲み物として、コーヒー、マンゴージュース、チャイ が選択できるとします。
class LunchSet < ActiveRecord::Base enum curry: { chicken: 0, vegitables: 2, beans: 3, shrimp: 4 } enum drink: [:coffee, :mango, :chai] end
class AddColumnToLunchSet < ActiveRecord::Migration def change add_column :lunch_sets, :drink, :integer end end
これで、curry
の場合と同様、lunch.drink = 'coffee'
や、lunch.mango?
などのメソッドが使用できます。
ここで、飲み物に野菜ジュースが追加されたとしましょう。
class LunchSet < ActiveRecord::Base enum curry: { chicken: 0, vegitables: 2, beans: 3, shrimp: 4 } enum drink: [:coffee, :mango, :chai, :vegitables] end
しかし、このコードは、vegitables
という名前を2回使っているため、vegitables?
などのメソッドが多重定義となり、エラーになります。
従って、複数のenum属性を使う場合は、例えば名前に接尾辞をつけるなどして、名前がかぶらないように注意してください。
class LunchSet < ActiveRecord::Base enum curry: { chicken_curry: 0, vegitables_curry: 2, beans_curry: 3, shrimp_curry: 4 } enum drink: [:coffee_drink, :mango_drink, :chai_drink, :vegitables_drink] end
将来追加される予定のオプション
GitHubにある最新のenumの実装をみてみると、enumに_prefix
、_suffix
というオプションが追加され、メソッド名の接頭辞・接尾辞を指定できるようです(trueの場合は、属性名と同じになる)。
まだ Rails 4.2.3 の時点ではリリースされていない機能ですが、先ほどのような名前がかぶる場合も、以下のようにスッキリ書けますね。
class LunchSet < ActiveRecord::Base enum curry: { chicken: 0, vegitables: 2, beans: 3, shrimp: 4 }, _suffix: true enum drink: [:coffee, :mango, :chai, :vegitables], _suffix: true end
対応する値は整数値のみ
そもそもenumについて調べたのは、Misocaにリマインダーメールの機能(新規登録時に、チュートリアルの案内が後日メールで送られてくる)を追加するためです。
リマインダーメールのモデルでは、最初は以下のようにenumを使用していました。
class TutorialReminder < ActiveRecord::Base enum status: { pending: 0, sent: 1, complete: 2 } end ※ 一部のみ抜粋
Pull Request のコードレビューをしてもらったところ、DBに保存する値は整数値ではなく文字列のままが良いということだったので、enumの名前に対応する値を以下のように文字列にしました。(普段からSQL等で直接DBに問い合わせを行うことがよく有り、その時に整数値よりも文字列の方が分かりやすいため)
class TutorialReminder < ActiveRecord::Base enum status: { pending: 'pending', sent: 'sent', complete: 'complete' } end
Rails 4.2.3 で動作確認したところ、このままでも期待通りに動作したので、このままenumを使う予定でした。
しかし、このブログを書くにあたってenumの実装などを確認した所、公式のRubyDocには、enumの名前に対応する値は整数値のみと書かれています。また、先ほど挙げた最新の実装を見ると、整数値に依存したコードも追加されているようなので、結局今回のリマインダーメールのモデルでは、enumを使うのは断念しました。
追記(2016/02/03):改めて最新のenumの実装を確認した所、DBに保存する値を文字列にした場合のテストケースなどが追加されていました。整数値以外の場合にも対応する予定なのかもしれません。
まとめ
教訓:ライブラリの機能を使う場合は、公式のAPIドキュメントをしっかり読んで使いましょう。
ちなみにムガルパレスですが、実際にはランチセットに野菜ジュースは存在しないです。また、ナンとライスの選択もできて、しかもナンとライスはおかわり自由です。ナンは大きめサイズなので、おかわりは計画的に!