ActiveRecordのenumで気をつけたい3つのポイント

初投稿の@sunflatです。好きなプログラミング言語MSX-BASIC です。

Rails 4.1でActiveRecordに追加された enum について、ちょっと調べてみました。

本当は、Misocaの開発でenumを使ってその実例を紹介する予定だったのですが、後述する理由により今回は適していなかったので使いませんでした。

そこで今回は、(株)Misocaの近くにある「ムガルパレス」というインドカレー屋さんの、ランチセットのメニューを例に、enumを使う時に気をつけたいポイントをいくつか紹介します。

enum の使い方

ActiveRecordenum を使うと、プログラムからは文字列(名前)でアクセスでき、DBには整数値で保存される属性を作成できます。

f:id:sunflat:20150810103023j:plain

さっそくランチセットのモデルを作成しましょう。カレーの種類として、チキン、ポーク、野菜、豆 を選択できるとします。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属性を持つ場合の注意

せっかくのランチセットなので、飲み物も追加しましょう。

f:id:sunflat:20150810103045j:plain

飲み物として、コーヒー、マンゴージュース、チャイ が選択できるとします。

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の実装はこちら

対応する値は整数値のみ

そもそも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に保存する値を文字列にした場合のテストケースなどが追加されていました。整数値以外の場合にも対応する予定なのかもしれません。

まとめ

f:id:sunflat:20150810103059j:plain

教訓:ライブラリの機能を使う場合は、公式のAPIドキュメントをしっかり読んで使いましょう。

ちなみにムガルパレスですが、実際にはランチセットに野菜ジュースは存在しないです。また、ナンとライスの選択もできて、しかもナンとライスはおかわり自由です。ナンは大きめサイズなので、おかわりは計画的に!