Rails 4.1で導入されるActiveRecord Enumsに隠された罠

TL;DR

ActiveRecord::Enumで、安易に値を追加・削除するのは危険。将来の変更に備えて、DBに登録される値をHashで指定しましょう。

class User < ActiveRecord::Base
  # This is BAD
  enum authority: [:registrant, :admin]
  # This is OK
  enum authority: { registrant: 10, admin: 20 }
end

本編

年度も変わりさて心機一転、という季節なのに私の地元は昨日大雪でしたが、皆様いかがお過ごしでしょうか。さて今日は表題の通り、Rails 4.1における目玉のひとつ、ActiveRecord Enumsについてです。

ActiveRecord Enumsとは

例えばUserモデルにauthority(権限)という属性を持たせたい時によくやるのは、DBにintegerのフィールドを用意して、0なら登録ユーザー、1なら管理者、...とする方法ですね。 この方法は便利ですが、0とか1とかいう値をそのまま数値で管理し続けるのは辛いので、scope :registrant, -> { where(authority: 0 }みたいなscopeとか、その他のユーティリティメソッドを自分で用意する必要がありました。

そこでActiveRecord Enumsを用いると、

class User < ActiveRecord::Base
  enum authority: [:registrant, :admin]
end

と定義してやることで、自動的に

User.registrant  # scope :registrant, -> { where(authority: 0) }

u = User.registrant.first
u.authority      # => "registrant"
u.admin!         # u.update_attribute(:authority, 1)

User.authorities # => { registrant: 0, admin: 1 }

みたいな便利メソッドがごそっとまとめて定義されます。

参考:ActiveRecord::Enum

問題点

enum authority: [:registrant, :admin]

と定義すると、DBに登録される値は自動的にregistrantは0、adminは1に設定されます。 この状態で、rails consoleからadminユーザーを一人登録しておきます。

u = User.new(name: "hoge", authority: User.authorities[:admin])
u.save!
u.authority # => "admin"

さてここで、サイトにゲスト権限を設定したくなった場合はどうなるでしょうか。自然に変更するとすれば仮にリストの先頭に加えてしまうと

enum authority: [:guest, :registrant, :admin]

となりますね。

2014-05-11追記

はてブコメントにて何件か「なぜ末尾ではなく先頭に足すのが『自然』なのか」というご指摘をいただきましたが、意図としてはauthorityの低い順に「ゲスト、登録ユーザ、管理者」と並んでいる方が「登録ユーザ、管理者、ゲスト」という並びよりも自然かなというものでした。
とはいえ、やはりenumの何たるかを考えると、後ろに足すのが普通だというご指摘は明らかに的を射ていますので、本文を修正してあります。
---追記ここまで---

この時、先ほど登録したhogeさんの状態を確認してみましょう。

u = User.find_by(name: "hoge")
u.authority # => "registrant"

なんと!hogeさんがadminからregistrantに格下げされてしまいました!

メカニズム

最初に

enum authority: [:registrant, :admin]

の状態でhogeさんをadminとして登録した時、DBには1という値が保存されています。決して"admin"という文字列が保存されているわけではありません。integerのカラムなのですから当然です。

先程は、この状態でmodel

enum authority: [:guest, :registrant, :admin]

と書き換えた結果、DBに保存されている1という値が示すauthorityregistrantになってしまっています。つまり

User.authorities # => { guest: 0, registrant: 1, admin: 2 }

というわけです。そのため、hogeさんのレコードが持っていた1という値により、hogeさんはregistrantであるとされてしまいました。

解決策

ActiveRecord Enumsには、DBに保存される値をHashで指定できます。

最初(guestがなかった頃)に

enum authority: { registrant: 10, admin: 20 }

としておけば、adminとして登録したhogeさんのレコードには20という値が保存されます。

これならば、guestを以下のように追加しても問題ありません。

enum authority: { guest: 5, registrant, 10, admin: 20 }

ユーザーの権限の種類なんて最初っからきちんと設計しろよ!という話も無くはありませんが、変更は常に起こりうるものです。今回のケースでは10文字くらい多くタイプするだけで変更耐性を付けられるのですから、ぜひとも採用しておきたいものだと思います。