Railsでも堅いデータベース設計をする。堅いenumの使い方


## はじめに

Rails4.1からenumが追加され、便利に使えるようになりました。

参考: [いまさらながらRails4.1から導入されたEnumが便利なのでまとめてみた](http://ruby-rails.hatenadiary.com/entry/20150710/1436461745)

しかしenumは、想定外の値が挿入されることをデータベースのレイヤーで防ぐことはできず、[SQLアンチパターン](https://www.oreilly.co.jp/books/9784873115894/) の第10章「31のフレーバー」のアンチパターンに当てはまります。

このような時は、enumに挿入される値をあらかじめ設定するマスターテーブルのようなものを作る必要があります。

そのサンプルを紹介します。

サンプルコードは、[こちらのgithub](https://github.com/kawasin73/rails_enum_sample)にアップロードしています。

## 開発環境

- Rails: `5.0.0.1`
- Database: `PostgreSQL 5.2`
- 環境は`docker-compose`でセットアップします。

開発を始める際は、

```bash
git clone https://github.com/kawasin73/rails_enum_sample.git
cd rails_enum_sample.git
docker-compose up -d spring
docker-compose exec spring rails db:create
docker-compose exec spring rails db:migrate
docker-compose exec spring rails db:seed_fu
docker-compose up
```

### 主要なgem

seed_fu をマスターデータの挿入に利用します。

## モデルの作成

今回は、`id`, `title`, `mode`, `category`の４つのカラムを持った`Item`モデルを作成します。

- `mode` は、`integer` を持つenum
- `category` は、`string` を持つenum

にする予定です。

### ItemMode モデル

`mode` のマスターデータを保持するための、`ItemMode`モデルを作成します。

```ruby:db/migrate/20170120005654_create_item_modes.rb
class CreateItemModes < ActiveRecord::Migration[5.0]
  def change
    create_table :item_modes, id: :integer do |t|
      t.string :name, null: false
    end
  end
end
```

`id` は `integer`型ですが、AUTO_INCREMENT を無効にするために、`create_table :item_modes, id: :integer do |t|` と指定しています。

```ruby:app/models/item_mode.rb
# == Schema Information
#
# Table name: item_modes
#
#  id   :integer          not null, primary key
#  name :string           not null
#

class ItemMode < ApplicationRecord
  validates :name, presence: true
end
```

### ItemCategory モデル

`category` のマスターデータを保持するための、`ItemCategory`モデルを作成します。

```ruby:db/migrate/20170120005937_create_item_categories.rb
class CreateItemCategories < ActiveRecord::Migration[5.0]
  def change
    create_table :item_categories, id: :string do |t|
    end
  end
end
```

```ruby:app/models/item_category.rb
# == Schema Information
#
# Table name: item_categories
#
#  id :string           not null, primary key
#

class ItemCategory < ApplicationRecord
end
```

### Item モデル

最後にItemモデルを作成します。

```ruby:db/migrate/20170120010046_create_items.rb
class CreateItems < ActiveRecord::Migration[5.0]
  def change
    create_table :items do |t|
      t.string :title, null: false
      t.integer :mode, null: false, default: 0
      t.string :category, null: false

      t.timestamps
    end

    add_foreign_key :items, :item_modes, column: :mode, primary_key: :id
    add_foreign_key :items, :item_categories, column: :category, primary_key: :id
  end
end
```

ここで重要となるのが、`add_foreign_key` 外部キー制約の設定です。これによって、マスターデータに設定されている値以外をデータベースに挿入できなくします。

```ruby:app/models/item.rb
# == Schema Information
#
# Table name: items
#
#  id         :integer          not null, primary key
#  title      :string           not null
#  mode       :integer          default(0), not null
#  category   :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
# Foreign Keys
#
#  fk_rails_28ef78c62c  (mode => item_modes.id)
#  fk_rails_4fb37c41fb  (category => item_categories.id)
#

class Item < ApplicationRecord
  validates :title, presence: true

  enum mode: { pending: 0, completed: 1, error: 2 }
  enum category: { politics: 'Politics', economy: 'Economy', sports: 'Sports' }
end
```

enum を設定します。`mode` はデフォルト値が`0`のため、`Item.new`した段階で自動的に`pending`になるのも嬉しいところです。

また、`ItemMode`と、`ItemCategory`は、データベースの堅牢性のためだけに作成したモデルですので、 **アプリケーションの中では利用しません。** そのため、`belongs_to` や `has_many` はあえて設定しません。

## マスターデータの作成

マスターデータは、seed_fuで作成します。

```ruby:db/fixtures/001_item_mode.rb
Item.modes.each do |name, id|
  ItemMode.seed do |item_mode|
    item_mode.id = id
    item_mode.name = name.to_s
  end
end
```

```ruby:db/fixtures/002_item_category.rb
Item.categories.values.each do |id|
  ItemCategory.seed do |item_category|
    item_category.id = id
  end
end
```

この上で以下のコマンドを実行するとマスターデータが作成できます。

```bash
docker-compose up -d spring
docker-compose exec spring rails db:seed_fu
```

また、新しい値を設定する際は、`Item`モデルのenumのハッシュを足して、`rails db:seed_fu`するだけでマスターデータに追加することができます。

## 最後に

以上の設定で、enumを堅いデータベース設計の中で利用することができます。
ぜひ参考にしてください。

## 参考URL

- [github - kawasin73/rails_enum_sample](https://github.com/kawasin73/rails_enum_sample)
- [いまさらながらRails4.1から導入されたEnumが便利なのでまとめてみた](http://ruby-rails.hatenadiary.com/entry/20150710/1436461745)
- [SQLアンチパターン](https://www.oreilly.co.jp/books/9784873115894/)
- [Rails だって硬いデータベース設計をしたい！そんなあなたに贈る Tips 4 選 - peroli Developer's Blog](http://tech.pero.li/entry/2016/09/09/100000) -> この記事は大変参考になった記事だったのですが、例の事件を受けてperoliさんのテックブログまで閉鎖されてしまったため現在は見ることができません。復活を祈ります。