[Rails] enumのvalueはStringにした方がいい
概要
Rails の enum を業務で数年使ってみて、「 value を String にした方がいいのではないか?」と考えて実践しているので、その根拠を示します。
対象読者
Rails の enum について知っている・使ったことがある方。
Rails の enum の基本的な知識についてはこの記事では触れません。
enumを使うモチベーション
-
DBで正規化していないので、joinしなくて済む - Gem を用いることで多言語対応が容易である
-
enumで定義されている値以外は入らない - 便利なメソッドが使えるようになる(※1)
※1. 以下のようなメソッド
Order.first.payment_status_success?
=> false
Order.payment_status_success
Order Load (6.9ms) SELECT "orders".* FROM "orders" WHERE "orders"."payment_status" = $1 [["payment_status", "success"]]
enumのvalueをIntegerで使うデメリット
-
DBを見てその値が何を意味するのか判断できない(モデルクラスのenumを確認する必要がある) -
enumのkeyとvalueがIntegerとStringとなっていて、各所のコードでどちらで扱っているかわかりづらい
2 に関しては、ネットワーク越しに受け取った値や環境変数など型情報が失われて、数値として扱いたい値を "1" のように文字列型として扱わざるを得ないシーンが多々あるので、 enum を使う際には Integer, String に加えて String (数字) の3つの値を意識する必要があります。
ActiveRecord のアトリビュートとして扱う際は暗黙的にキャストしてくれますが、 例えば enum の特定の値であるか? という判定をする際などには Integer や String として比較することになるので、注意して実装しないとミスをしてしまい、プロダクトに不具合が起きかねません。
enum の value を Integer で使いたくない最大の理由はこの 2 にあります。
enumのvalueをIntegerで使うコード
# app/models/order.rb
class Order < ActiveRecord::Base
# 決済状態
enum payment_status: {
yet: 0, # 未決済
success: 1, # 決済成功
failed: 2, # 決済失敗
cancel: 3 # キャンセル
}, _prefix: true
end
# config/locales/ja.yml
ja:
enums:
order:
payment_status:
yet: 未決済
success: 決済成功
failed: 決済失敗
cancel: キャンセル
# classからkey/valueの一覧を取得
Order.payment_statuses
=> {"yet"=>0, "success"=>1, "failed"=>2, "cancel"=>3}
Order.payment_statuses.class
=> ActiveSupport::HashWithIndifferentAccess
# classからkeyの一覧を取得
Order.payment_statuses.keys
=> ["yet", "success", "failed", "cancel"]
# classからvalueの一覧を取得
Order.payment_statuses.values
=> [0, 1, 2, 3]
# classから特定のkeyを取得
Order.payment_statuses.invert[1]
=> "success"
# classから特定のvalueを取得
Order.payment_statuses[:success]
=> 1
# インスタンスからkeyを取得
Order.first.payment_status
=> "success"
# インスタンスからvalueを取得
Order.first.payment_status_before_type_cast
=> 1
# インスタンスから論理名を取得
Order.first.payment_status_i18n
=> "決済成功"
enumのvalueをStringで使うコード
# app/models/order.rb
class Order < ActiveRecord::Base
# 決済状態
enum payment_status: {
yet: 'yet', # 未決済
success: 'success', # 決済成功
failed: 'failed', # 決済失敗
cancel: 'cancel' # キャンセル
}, _prefix: true
end
# config/locales/ja.yml
ja:
enums:
order:
payment_status:
yet: 未決済
success: 決済成功
failed: 決済失敗
cancel: キャンセル
# classからkey/valueの一覧を取得
Order.payment_statuses
=> {"yet"=>"yet", "success"=>"success", "failed"=>"failed", "cancel"=>"cancel"}
Order.payment_statuses.class
=> ActiveSupport::HashWithIndifferentAccess
# classからkeyの一覧を取得
Order.payment_statuses.keys
=> ["yet", "success", "failed", "cancel"]
# classからvalueの一覧を取得
Order.payment_statuses.values
=> ["yet", "success", "failed", "cancel"]
# classから特定のkeyを取得
Order.payment_statuses.invert[1]
=> "success"
# classから特定のvalueを取得
Order.payment_statuses[:success]
=> "success"
# インスタンスからkeyを取得
Order.first.payment_status
=> "success"
# インスタンスからvalueを取得
Order.first.payment_status_before_type_cast
=> "success"
# インスタンスから論理名を取得
Order.first.payment_status_i18n
=> "決済成功"
まとめ
kay も value も String なのであれば、 enum ではなく、ただの String 型のカラムにして、リストでバリデーションをかければいいのでは? と指摘されそうですが、 enum を使うことにより多言語対応が容易であり便利なメソッドが使えるようになるので、入る値が決まっているのであれば、 String 型のカラムよりも enum の value を String で使う方が良いと思います。
DB のカラムでは String よりも Integer の方が高速なので、速度が求められるシーンでは value を Integer にした方がいいかもしれません。
この記事の内容に問題点があったり、より良い方法があるならコメントでご教示いただけると幸いです。
Discussion