✏️

[Rails] enumのvalueをStringにした方がいいと感じた話

2022/09/17に公開約3,700字

概要

Railsenum を業務で数年使ってみて、「 valueString にした方がいいのではないか?」と感じたので、その根拠を示します。

この方針を業務で運用した実績はないので、実際に使ったらそのときに記事を更新します。

対象読者

Railsenum について知っている・使ったことがある方。
Railsenum の基本的な知識についてはこの記事では触れません。

enumを使うモチベーション

  1. DB で正規化していないので、 join しなくて済む
  2. Gem を用いることで多言語対応が容易である
  3. enum で定義されている値以外は入らない
  4. 便利なメソッドが使えるようになる(※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で使うデメリット

  1. DB を見てその値が何を意味するのか判断できない(モデルクラスの enum を確認する必要がある)
  2. enum の keyvalueInteger と String となっていて、各所のコードでどちらで扱っているかわかりづらい

2 に関しては、ネットワーク越しに受け取った値や環境変数など型情報が失われて、数値として扱いたい値を "1" のように文字列型として扱わざるを得ないシーンが多々あるので、 enum を使う際には Integer, String に加えて String (数字) の3つの値を意識する必要があります。
ActiveRecord のアトリビュートとして扱う際は暗黙的にキャストしてくれますが、 例えば enum の特定の値であるか? という判定をする際などには IntegerString として比較することになるので、注意して実装しないとミスをしてしまい、プロダクトに不具合が起きかねません。

enumvalueInteger で使いたくない最大の理由はこの 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
=> "決済成功"

まとめ

kayvalueString なのであれば、 enum ではなく、ただの String 型のカラムにして、リストでバリデーションをかければいいのでは? と指摘されそうですが、 enum を使うことにより多言語対応が容易であり便利なメソッドが使えるようになるので、入る値が決まっているのであれば、 String 型のカラムよりも enumvalueString で使う方が良いと思います。

DB のカラムでは String よりも Integer の方が高速なので、速度が求められるシーンでは valueInteger にした方がいいかもしれません。

この記事の内容に問題点があったり、より良い方法があるならコメントでご教示いただけると幸いです。

Discussion

ログインするとコメントできます