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 }
みたいな便利メソッドがごそっとまとめて定義されます。
問題点
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
という値が示すauthority
はregistrant
になってしまっています。つまり
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文字くらい多くタイプするだけで変更耐性を付けられるのですから、ぜひとも採用しておきたいものだと思います。