Vibloというフランジアが運営する技術系トピックを共有のためのメディアの中から、フランジアベトナムの社員による記事をピックアップして紹介します。
今回の記事はフランジアのベトナム・ハノイオフィスSytem Development Division 1でRuby on Railsエンジニアとして活躍しているTa Duy Anhさんによる「RailsのEnum:諸刃の剣」です。
RailsのEnum: 諸刃の剣
Ruby On Railsと付き合っている人にとってEnumは新しくない概念のはずです。Railsに入れるEnumの目的は単純な数を変えて分かりやすい言葉などで書かれる構文(コード)になることです。その一方、Enumを特性を十分理解した上で使わないと危険を招くかもしれません。
この記事では私が今まで何百回も遭遇した”500
エラー”の中から得られたEnumの使い方の留意点を述べていきたいと思います。
Enum とは?
RailsのAPI Documentの定義によると:
Declare an enum attribute where the values map to integers in the database, but can be queried by name
enum属性はデータベースの整数型にマッピングされ、textから参照可能にします。
Default Value
以下の例を見てみましょう。
Railsで Human
というModelを定義します:
class Human < ActiveRecord::Base enum status: [ :dead, :alive ] end
ご存じの通り、Human
のクラスの中に status
という属性があり、データベースでのstatus
と対応しています。これが2つのステータスを受け取り、:dead
と :alive
と定義されているシンボルに対応します。
上記の構成でRailsは自動的に:dead
に0を、:alive
に1を与えます。そして、Human
モデルと対応するhuman
表を作成する為にマイグレーションします:
class CreateHuman < ActiveRecord::Migration def change create_table :human do |t| t.string :name t.integer :status end end end
マイグレートするとコンソールしてから、以下のような結果が出てきます:
irb(main):024:0> h = Human.new => #irb(main):025:0> h.dead? => false irb(main):026:0> h.alive? => false irb(main):027:0>
Human
インスタンスが生成されたら、enumで
定義された2つの値と対応するdead?
と alive?
の関数が追加されます。
インスタンスのステータスがnil
なので、この2つの関数からは false
が帰ってきます。つまり、この「人」は生きているわけでなく、死んでいるわけでもないと言うことになります。 (Dead or Alive, no I’m the other)
enumの危険性
「もしenumとするフィールドのデフォルト値を定義しないと、構成されたときの値がnilとなり、その値に与える全ての関数がFalseを返す」
これが1つ目のenumの危険性です。
ステータスが表示される以下のロジックがあるとしましょう:
render "出生証明書" if h.alive? render "死亡通知" if h.dead?
ここで出生証明書も死亡通知も出てこない理由はh
インスタンスが生きているわけでも死んでいるわけでもないからです。
ロジックを以下のように変えてみましょう:
if h.alive? render "出生証明書" else render "死亡通知" end
または:
if h.dead? render "死亡通知" else render "出生証明書" end
上記のように変更した場合、状況はより 悪化します。ステータスがnil
か dead
/alive
であれば出生証明書または死亡通知が出てきます。
ここで、私たちは評価の順番によって出力値が異なるという矛盾した状況に遭遇することになり、これは発見が難しいBugの原因になります。
ここで私たちエンジニアはどのような対策をすればよいでしょうか?
私のおすすめはenumを利用する場合はデフォルト値を定義し、nil
の結果になる可能性を全て排除する、つまりnull: false
とすることです。
以下のようにマイグレーションを変えてみましょう:
class CreateHumen < ActiveRecord::Migration def change create_table :humen do |t| t.string :name t.integer :status, null: false, default: 0 end end end
次にマイグレーションを再度動かしてコンソールでテストしましょう:
irb(main):005:0> Human.new.dead? => true irb(main):006:0> Human.new.alive? => false irb(main):007:0> Human.new.status => "dead" irb(main):009:0> Human.new => #
上記コードでは、ステータスのフィールドは0のデフォルト値が与えられています。
Humanインスタンス(デフォルトはdead
)を構築をするときのオペレーションは安全です。
その上、nilと指定してるのにDBに保存される危険性を避ける為、null: false
を追加すします。
実装は以下になります:
irb(main):013:0> h = Human.new => #irb(main):014:0> h.status = nil => nil irb(main):015:0> h.save (0.1ms) begin transaction SQL (0.4ms) INSERT INTO "human" ("status") VALUES (?) [["status", nil]] SQLite3::ConstraintException: NOT NULL constraint failed: humen.status: INSERT INTO "human" ("status") VALUES (?) (0.0ms) rollback transaction
上記のコードから、 ステータスフィールドに対してnil
を与えることができることがわかります。次に、DBにnull: false
が無い場合、ステータスフィールドにnil
を与えることはいつでもできるので先ほど述べてきたロ
ジックと同じになってしまいます。
Railsでenumを使うときの危険性
Railsのenumを使うときの一つめの危険性は
「もしenumとするフィールドのデフォルト値を定義しないと、構成されたときの値がnil
となり、その値に与える全ての関数がFalse
を返す」
でした。
この問題に対するわたしからのアドバイスは
「enumを使うことにしたら デフォルト値を設定して絶対にnil
を避けるようにしましょう。」
になります。
次回も引き続き、Ralisでenumを使う際に気をつけなければならないポイントについてTa Duy Anhさんの記事を紹介します。