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

上記のように変更した場合、状況はより 悪化します。ステータスがnildead/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さんの記事を紹介します。

フランジアのRuby on Railsエンジニア アインさん【著者紹介】
Ta Duy Anhさんはベトナム理系トップ大学であるハノイ工科大を卒業後、四年間プログラマーとして様々なソフトウェア開発現場を経験してきました。現在はRubyやJavascriptを使った開発に従事しており、特にRuby on Railsを得意としています。

 

オフショア開発のお問合せ