🎶

[Rails]同じEnumを複数モデルで使う

2022/03/27に公開

概要

同じ ActiveRecord::Enum を複数モデルで使う方法を以下に示します。

何も工夫しなくても、全く同じコードを複数のモデルに記述することで実現できるのですが、それではDRY原則に反しており、概念として同じものを指していることがコードで表現できていません。
このようなコードは変更の際に変更漏れが生じたり、あるいは一方のモデルには要素を追加してもう一方には追加しないといった当初の設計意図(概念として同じものを指している)に反した変更が加えられるリスクが生じます。

これらの問題を解決しつつ、同じ ActiveRecord::Enum を複数モデルで使う方法を考えます。

方法

モデルクラスの外で定義した Hash を enum に与えます。

# config/settings.yml

prefectures:
  hokkaido: 1
  aomori: 2
  iwate: 3
  # 省略
# app/models/user.rb

class User < ActiveRecord::Base
  enum prefecture: Settings.prefectures.to_h
end
# app/models/address.rb

class Address < ActiveRecord::Base
  enum prefecture: Settings.prefectures.to_h
end

今回の例は 都道府県 というドメイン知識を持たない情報なので、 settings.yml に定義しています。
ドメイン知識を持つ情報の場合、 models 以下に class を作ることも検討しましょう。

# app/models/tax_rate.rb

# 税率
class TaxRate
  TYPES = {
     no: 1, # 0%
    normal: 2, # 10%
    reduced: 3 # 8%
  }.freeze
end
# app/models/product.rb

class Product < ActiveRecord::Base
  enum tax_rate: TaxRate::TYPES
end

上記の例だと、 ActiveSupport::Concern で同じことをできますが、その場合、 enum のカラム名を異なる名前で定義できないですし、ひとつのモデルクラスで複数の enum カラムがあり、それらで同じ要素を扱いたいときに対応できないです。

# app/models/order.rb

class Order < ActiveRecord::Base
  # 宛先都道府県
  enum dest_prefecture: Settings.prefectures.to_h, _prefix: true
  # 送り元都道府県
  enum origin_prefecture: Settings.prefectures.to_h, _prefix: true
end

特定のモデルにだけ要素を追加したい場合

同じ ActiveRecord::Enum を複数モデルで使いたいが、特定のモデルでだけ enum に要素を追加したい場合、以下のように実装します。

# app/models/user.rb

class User < ActiveRecord::Base
  enum prefecture: {
    unselected: 0, # 未選択
    **Settings.prefectures.to_h
  }
end

Settings.prefectures.to_h に 未選択 を追加しています。
この書き方では、 value の重複が起きかねないですし、わかりづらくなってしまう場合があるので、積極的には採りたくはないです。

特定のモデルでだけ特定の要素を除外したい場合

同じ ActiveRecord::Enum を複数モデルで使いたいが、特定のモデルでだけ enum の特定の要素を除外したい場合、以下のように実装します。

# app/models/subscription_status.rb

class SubscriptionStatus
  TYPES = {
     free: 1, # 無料会員
    light: 2, # ライトプラン
    standard: 3, # スタンダードプラン
    pro: 4 # プロフェッショナルプラン
  }.freeze
end
# app/models/enterprise_user.rb

# 企業ユーザー
class EnterpriseUser < ActiveRecord::Base
  enum prefecture: SubscriptionStatus::TYPES.except(:free)
end

.except() で除外したい要素を取り除いています。
(この例の場合だと除外したい要素を取り除いて返す SubscriptionStatus#enterprise_types みたいなメソッドを定義した方がよさそうですが)

この書き方では、わかりづらくなってしまう場合があるので、積極的には採りたくはないです。

enum_help で翻訳したい場合

今回示した方針で実装した際に enum_help で enum を翻訳したい場合、以下のように ja.yml に定義する。

# config/locales/ja.yml

ja:
  # 都道府県
  prefecture: &prefecture
    hokkaido: '北海道'
    aomori: '青森県'
    iwate: '岩手県'
    # 省略
  enums:
    user:
      prefecture: *prefecture

yml の記法である & アンカー および * エイリアス を利用します。
& アンカー で再利用したい要素を指定し、 * エイリアス で再利用します。

Discussion