【Ruby on Rails】ちょっとしたマスタデータの管理

## はじめに
Railsアプリケーションの実装で新たにマスタデータを持つ必要が出てきた時に「わざわざテーブルを作りたくない」と思ったことはないでしょうか。
「他テーブルと結合して使うわけでもない」「基本的に更新されない（ほぼ固定値のようなもの）」「データ数も多くない」といった条件が重なってくると、マスタ管理のためだけにテーブルを増やすのは少し抵抗があります。
そんな時に役立ちそうな方法をまとめてみました。

## 方法1, ActiveModel & クラス内に直接定義
`ActiveModel` を読み込み、マスタデータもクラス内で全て定義してしまう方法です。ごく一部の機能さえあれば良く、とにかくシンプルに済ませたい時に向いています。

```ruby:app/models/region.rb
class Region
  include ActiveModel::Model

  attr_accessor :code, :name

  DATA = [
    { code: "R01", name: "北海道地方" },
    { code: "R02", name: "東北地方" },
    { code: "R03", name: "関東地方" },
    { code: "R04", name: "中部地方" },
    { code: "R05", name: "近畿地方" },
    { code: "R06", name: "中国地方" },
    { code: "R07", name: "四国地方" },
    { code: "R08", name: "九州・沖縄地方" }
  ].freeze

  # 区域一覧を取得
  def self.all
    DATA.map { |item| self.new(item) }
  end
end
```
`self.all` で定義した通り、区域一覧を取得することができています。
```ruby
Region.all
=>
[#<Region:0x0000ffff81da9a40 @code="R01", @name="北海道地方">,
 #<Region:0x0000ffff81da9720 @code="R02", @name="東北地方">,
 #<Region:0x0000ffff81da96a8 @code="R03", @name="関東地方">,
 #<Region:0x0000ffff81da9630 @code="R04", @name="中部地方">,
 #<Region:0x0000ffff81da95b8 @code="R05", @name="近畿地方">,
 #<Region:0x0000ffff81da9540 @code="R06", @name="中国地方">,
 #<Region:0x0000ffff81da94c8 @code="R07", @name="四国地方">,
 #<Region:0x0000ffff81da9428 @code="R08", @name="九州・沖縄地方">]
```
**Pros**
- `gem` も不要かつ1ファイルのみで完結するためとにかくシンプル

**Cons**
- マスタデータの量や項目が多いとコードの見通しが悪くなる
- 必要な処理は自分で書く必要がある


## 方法2, ActiveModel & YAMLファイル
`ActiveModel` を使いつつマスタデータをYamlファイルから読み込むように変更したものです。「クラス内でマスタデータを定義するには量や項目が多い」といった場合はこちらの方が良いでしょう。
```ruby:app/models/region.rb
class Region
  include ActiveModel::Model

  attr_accessor :code, :name

  # yamlからデータをロード
  def self.data
    @data ||= YAML.load_file(Rails.root.join('config', 'master_data', 'regions.yml'))
  end

  # 区域一覧を取得
  def self.all
    data.map { |item| self.new(item) }
  end
end
```

```yaml:config/master_data/regions.yml
- code: R01
  name: 北海道地方
- code: R02
  name: 東北地方
- code: R03
  name: 関東地方
- code: R04
  name: 中部地方
- code: R05
  name: 近畿地方
- code: R06
  name: 中国地方
- code: R07
  name: 四国地方
- code: R08
  name: 九州・沖縄地方
```
**Pros**
- データとコードが分離されているため管理しやすい
- データが増えてもコードの可読性を損なわない

**Cons**
- 必要な処理についてはやはり自分で書く必要がある


## 方法3, ActiveHash & クラス内に直接定義
gem `active_hash` を利用する方法です。gemをインストールする必要はありますが、1ファイルで完結する上に `ActiveRecord` と同じ感覚で使うことができます。

https://github.com/active-hash/active_hash

導入も簡単。まずはgemをインストールします。
```ruby
gem 'active_hash'
```

そして、`ActiveHash::Base` を継承してクラスを作成します。`self.data` でマスターデータを定義してください。
```ruby:app/models/region.rb
class Region < ActiveHash::Base

  self.data = [
    { code: "R01", name: "北海道地方" },
    { code: "R02", name: "東北地方" },
    { code: "R03", name: "関東地方" },
    { code: "R04", name: "中部地方" },
    { code: "R05", name: "近畿地方" },
    { code: "R06", name: "中国地方" },
    { code: "R07", name: "四国地方" },
    { code: "R08", name: "九州・沖縄地方" }
  ]
end
```

`all`、 `find_by` 等、`ActiveRecord` でよく使う処理と同様のことができます。
```ruby
Region.all
=>
[#<Region:0x0000ffff80b895e0 @attributes={:code=>"R01", :name=>"北海道地方", :id=>1}>,
 #<Region:0x0000ffff80b88bb8 @attributes={:code=>"R02", :name=>"東北地方", :id=>2}>,
 #<Region:0x0000ffff80b88730 @attributes={:code=>"R03", :name=>"関東地方", :id=>3}>,
 #<Region:0x0000ffff80b88348 @attributes={:code=>"R04", :name=>"中部地方", :id=>4}>,
 #<Region:0x0000ffff80b87f60 @attributes={:code=>"R05", :name=>"近畿地方", :id=>5}>,
 #<Region:0x0000ffff80b87b78 @attributes={:code=>"R06", :name=>"中国地方", :id=>6}>,
 #<Region:0x0000ffff80b87790 @attributes={:code=>"R07", :name=>"四国地方", :id=>7}>,
 #<Region:0x0000ffff80b873a8 @attributes={:code=>"R08", :name=>"九州・沖縄地方", :id=>8}>]
 
Region.find_by(code: "R03")
=> #<Region:0x0000ffffa1e48828 @attributes={:code=>"R03", :name=>"関東地方", :id=>3}>
```

**Pros**
- `ActiveRecord` と同様の感覚で利用することができる
- 1ファイルで完結するためシンプル

**Cons**
- gemへの依存は生まれる
- マスタデータの量や項目が多いとコードの見通しが悪くなる

## 方法4, ActiveHash & YAMLファイル
gem `active_hash` をインストールすると、YAMLファイルをロードして `ActiveHash` オブジェクトとして扱えるようにする `ActiveYaml` クラスを使えるようになります。
`ActiveYaml::Base` を継承してクラスを作成し、`set_root_path` でマスタデータのYAMLファイルを設置しているディレクトリを、`set_filename` で読み込むYAMLファイルを指定してください。
```ruby:app/models/region.rb
class Region < ActiveYaml::Base
  set_root_path Rails.root.join("config", "master_data")
  set_filename "regions"
end
```

方法3と同様に、`ActiveRecord` と同じ感覚で操作ができます。
```ruby
Region.find_by(code: "R02")
=> #<Region:0x0000ffff803b1ea8 @attributes={:code=>"R02", :name=>"東北地方", :id=>2}>
```

**Pros**
- `ActiveRecord` と同様の感覚で利用することができる
- データとコードが分離されているため管理しやすい
- データが増えてもコードの可読性を損なわない

**Cons**
- gemへの依存は生まれる

## まとめ
マスタデータの量や項目、必要としている機能や実装の手間に応じて適した方法を選択するのが良いでしょう。コードの可読性という観点から、データについてはYAMLファイルで管理した方が感覚的にスッキリはしました。
4つ全てを試した感じでは **「方法4, ActiveHash & YAMLファイル」** がコードの見通しもよく実装の手間も省けて良かったのですが、「どうしてもgemへの依存を避けたい」というような場合は `ActiveModel` を使って自分で実装するという選択肢も生まれてくるかと思います。


## 参考
[GitHub - active-hash/active_hash](https://github.com/active-hash/active_hash)
[【Rails】ActiveHashでマスターデータを管理する](https://www.mof-mof.co.jp/tech-blog/2024/10/30/100000)