[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