読者です 読者をやめる 読者になる 読者になる

すがブロ

sugamasaoのhatenablogだよ

Rails 4.1のenumの挙動

みんな大好きenum

DBのカラムでステータスとか使うとき、ぼくは数値派なんですけど、enumで掛けたら楽で良いですよねみたいのあります。ありますよね。

で、Rails 4.1ではそういうのが導入されたっぽいので試してみました。

rails 4.1をインストールしてプロジェクト作ったりする

$ gem install rails --pre --no-ri --no-rdoc
$ rails new sample
$ cd sample
$ ./bin/rails g scaffold product name:string status:integer
$ ./bin/rake db:migrate

rails cでフツーに登録してみる(ここは何の変哲も無い)。

irb(main):005:0> Product.create!(name: "hoge", status: 99)
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-03-26 01:25:21.026233"], ["name", "hoge"], ["status", 99], ["updated_at", "2014-03-26 01:25:21.026233"]]
   (1.9ms)  commit transaction
=> #<Product id: 2, name: "hoge", status: 99, created_at: "2014-03-26 01:25:21", updated_at: "2014-03-26 01:25:21">

statusの値が99でも保存できちゃう。そのままでも良いんだけど、validatesのおさらいとして例えば0〜2までに制限してみようか。

class Product < ActiveRecord::Base
  validates :status, inclusion: {in: 0..2}
end

ためしてみよう

Loading development environment (Rails 4.1.0.rc2)
irb(main):001:0> Product.create!(name: "hoge", status: 99)
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
ActiveRecord::RecordInvalid: Validation failed: Status is not included in the list
irb(main):003:0* Product.create!(name: "hoge", status: 2)
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-03-26 01:27:51.238248"], ["name", "hoge"], ["status", 2], ["updated_at", "2014-03-26 01:27:51.238248"]]
   (1.6ms)  commit transaction
=> #<Product id: 3, name: "hoge", status: 2, created_at: "2014-03-26 01:27:51", updated_at: "2014-03-26 01:27:51">

数値の範囲指定ができましたね。

それではenumを使ってみよう

enumの設定をしてみよう。ややこしくなるので、validatesは消します。ついでに書いておくと、enumの項目自体はテキトウなのでスルー推奨です。

class Product < ActiveRecord::Base
  enum status: {
    normal: 0, # 通常
    sale: 1,   # 特売
    empty: 2,  # 在庫切れ
  }
end

これで試してみよう。まずは正常系で。

irb(main):005:0* Product.create!(name: "hoge", status: 2)
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-03-26 01:33:06.501462"], ["name", "hoge"], ["status", 2], ["updated_at", "2014-03-26 01:33:06.501462"]]
   (1.6ms)  commit transaction
=> #<Product id: 6, name: "hoge", status: 2, created_at: "2014-03-26 01:33:06", updated_at: "2014-03-26 01:33:06">
irb(main):006:0> Product.create!(name: "hoge", status: :sale)
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-03-26 01:33:13.877152"], ["name", "hoge"], ["status", 1], ["updated_at", "2014-03-26 01:33:13.877152"]]
   (1.5ms)  commit transaction
=> #<Product id: 7, name: "hoge", status: 1, created_at: "2014-03-26 01:33:13", updated_at: "2014-03-26 01:33:13">
irb(main):005:0> Product.create!(name: "hoge", status: 'sale')
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-03-26 01:57:26.957196"], ["name", "hoge"], ["status", 1], ["updated_at", "2014-03-26 01:57:26.957196"]]
   (1.4ms)  commit transaction
=> #<Product id: 15, name: "hoge", status: 1, created_at: "2014-03-26 01:57:26", updated_at: "2014-03-26 01:57:26">

このように、シンボルや文字列、そして数値でも保存してくれるようになった。便利。

では範囲外の値を入力すると?

irb(main):007:0> Product.create!(name: "hoge", status: 99)
ArgumentError: '99' is not a valid status
irb(main):009:0* Product.create!(name: "hoge", status: :hoge)
ArgumentError: 'hoge' is not a valid status
irb(main):011:0* Product.create!(name: "hoge", status: 'hogehoge')
ArgumentError: 'hogehoge' is not a valid status

ことごとくArgumentErrorとなる。create!で試しているけど、createnewでも試してみよう。

irb(main):007:0> Product.create(name: "hoge", status: 99)
ArgumentError: '99' is not a valid status
irb(main):009:0* Product.new(name: "hoge", status: 99)
ArgumentError: '99' is not a valid status

create!だけに限らず、newメソッドとかでも引数に異なる範囲の値が入るとArgumentErrorになる。 普通のvalidatesに条件を書く場合は ActiveRecord::RecordInvalid: Validation failed: Status is not included in the list みたいにActiveRecord::RecordInvalidの例外クラスが送出されるけど、そうではない(例外クラス的にも、発生タイミング的にも)ので気をつけた方が良さそう。

validatesではないから挙動が異なるのは当然といえば当然だけど、使い勝手としては同じような挙動のほうが嬉しかったんだけどそうではなかったという備忘録エントリーです。

Rails詳しくないのでよくわかりませんが、ここら辺、正式リリースまでに挙動が変わったりするかしら?

書かなくてはならない事柄(追記)

こちらも何卒よろしくお願い致します。

Webアプリエンジニア養成読本[しくみ、開発、環境構築・運用…全体像を最新知識で最初から! ] (Software Design plus)

Webアプリエンジニア養成読本[しくみ、開発、環境構築・運用…全体像を最新知識で最初から! ] (Software Design plus)