Rails6 のちょい足しな新機能を試す113（MySQL enum set 編）

# はじめに

Rails 6 に追加された新機能を試す第113段。 今回は、`MySQL enum set` 編です。
Rails 6 では、MySQL の enum や set のカラムの schema dump が正しく出力されるようになりました。

Ruby 2.6.5, Rails 6.0.2.1, MySQL 8.0.16 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)

```shell
$ rails --version
Rails 6.0.2.1
```

今回は、users テーブル (User モデル）に `enum` と `set` のカラムを追加して試してみます。

# Rails プロジェクトを作成する

```shell
$ rails new rails_sandbox
$ cd rails_sandbox
```

# User モデルを作成する

`name` カラムだけもつ `User` モデルを作成します。

`enum` と `set` のカラムは、後で、migration ファイルを直接編集して、追加します。

```shell
bin/rails g model User name
```

# migration ファイルを編集する

migration ファイルを編集して、 `enum` カラムの `generation` と `set` カラムの `learning` を追加します。

```ruby:db/migrate/20191221002717_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.column :generation, "enum('baby', 'toddler', 'preschool', 'gradeschool', 'teen', 'young_adult')"
      t.column :learning, "set('piano', 'english', 'swimming', 'ballet', 'calligraphy')"

      t.timestamps
    end
  end
end
```

# seed データを作成する

機能を試すためには、作る必要は、無いのですが、一応、 seed データを1件作成します。

```ruby:db/seeds.rb
User.create(name: 'Dave', generation: 'gradeschool', learning: 'english,swimming')
```

# マイグレーションを実行する

マイグレーションを実行します。
```shell
$ bin/rails db:create db:migrate
```

# スキーマファイルを確認する

できたスキーマファイルを確認します。

```ruby:db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_21_002717) do

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name"
    t.column "generation", "enum('baby','toddler','preschool','gradeschool','teen','young_adult')"
    t.column "learning", "set('piano','english','swimming','ballet','calligraphy')"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

end
```

# Rails 5 では

Rails 5.2.4 では、スキーマファイルは以下のようになってしまいます。

`generation` と `learning` のカラムが string になってしまい、ENUM と SET であることがわからなくなってしまいます。

```ruby:db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_21_005503) do

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name"
    t.string "generation", limit: 11
    t.string "learning", limit: 41
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end
```

# MySQL で直接確認する

なお、MySQL で直接確認した場合は、Rails 6.0.2.1 も、Rails 5.2.4 も同じです。

```
mysql> show columns from users;
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| Field      | Type                                                                  | Null | Key | Default | Extra          |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| id         | bigint(20)                                                            | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)                                                          | YES  |     | NULL    |                |
| generation | enum('baby','toddler','preschool','gradeschool','teen','young_adult') | YES  |     | NULL    |                |
| learning   | set('piano','english','swimming','ballet','calligraphy')              | YES  |     | NULL    |                |
| created_at | datetime(6)                                                           | NO   |     | NULL    |                |
| updated_at | datetime(6)                                                           | NO   |     | NULL    |                |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
```

# bin/rails db:reset を試す

`bin/rails db:reset` を実行してから、MySQL で直接確認すると

Rails 6.0.2.1 は、

```
mysql> show columns from users;
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| Field      | Type                                                                  | Null | Key | Default | Extra          |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| id         | bigint(20)                                                            | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)                                                          | YES  |     | NULL    |                |
| generation | enum('baby','toddler','preschool','gradeschool','teen','young_adult') | YES  |     | NULL    |                |
| learning   | set('piano','english','swimming','ballet','calligraphy')              | YES  |     | NULL    |                |
| created_at | datetime(6)                                                           | NO   |     | NULL    |                |
| updated_at | datetime(6)                                                           | NO   |     | NULL    |                |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)
```

と変わりないですが、Rails 5.2.4 では

```
mysql> show columns from users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| generation | varchar(11)  | YES  |     | NULL    |                |
| learning   | varchar(41)  | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
```

と変わってしまいました。

# rails console からデータを検索する

(ここからはおまけです。)

`bin/rails db:reset` で seed データが登録されたので、検索してみます。

```irb
irb(main):006:0> User.where(generation: 'gradeschool')
  User Load (1.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`generation` = 'gradeschool' LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "Dave", generation: "gradeschool", learning: "english,swimming", created_at: "2019-12-21 02:04:41", updated_at: "2019-12-21 02:04:41">]>
irb(main):007:0> User.where("learning like '%swimming%'")
  User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE (learning like '%swimming%') LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "Dave", generation: "gradeschool", learning: "english,swimming", created_at: "2019-12-21 02:04:41", updated_at: "2019-12-21 02:04:41">]>
```

# 正しくないデータを投入する

generation や、learning に不正なデータを指定して登録すると `ActiveRecord::StatementInvalid` が発生します。

generation も learning も不正なデータの場合を試してみます。

最後のエラーメッセージが `Data truncated for column 'generation' at row 1` であることから `generation` に問題があることがわかります。
(が、どうして truncate されたか、ちょっとわかりにくいです...。)

```irb
irb(main):008:0> User.create(name: 'NG', generation: 'NG', learning: 'NG')
   (0.5ms)  BEGIN
  User Create (1.6ms)  INSERT INTO `users` (`name`, `generation`, `learning`, `created_at`, `updated_at`) VALUES ('NG', 'NG', 'NG', '2019-12-21 02:15:29.767037', '2019-12-21 02:15:29.767037')
   (0.4ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):8
ActiveRecord::StatementInvalid (Mysql2::Error: Data truncated for column 'generation' at row 1)
```

generation は正しいが、 learning が不正なデータの場合です。

最後のエラーメッセージが `Data truncated for column 'learning' at row 1` であることから `learning` に問題があることがわかります。

```irb
irb(main):009:0> User.create(name: 'NG', generation: 'gradeschool', learning: 'NG')
   (0.4ms)  BEGIN
  User Create (1.2ms)  INSERT INTO `users` (`name`, `generation`, `learning`, `created_at`, `updated_at`) VALUES ('NG', 'gradeschool', 'NG', '2019-12-21 02:15:50.398770', '2019-12-21 02:15:50.398770')
   (0.2ms)  ROLLBACK
Traceback (most recent call last):
        2: from (irb):9
        1: from (irb):9:in `rescue in irb_binding'
ActiveRecord::StatementInvalid (Mysql2::Error: Data truncated for column 'learning' at row 1)
```

## Rails 5 では

`bin/rails db:migrate` で作成した場合は、Rails 6 と同じ動作ですが、 `bin/rails db:reset` すると Type がただの string になってしまうため、不正なデータでも登録できてしまいます。

# ActiveRecord の enum との関係

ActiveRecord の `enum` と MySQL の `enum` は全く関係がなく、以下のように、 User モデルに `enum` を定義しても、MySQL の generation カラムの enum とは連動せず、期待通りの動作はしません。

```ruby:app/models/user.rb
class User < ApplicationRecord
  enum generation: %i[baby toddler preschool gradeschool teen young_adult]
end
```

# 試したソース

https://github.com/suketa/rails_sandbox/tree/try113_mysql_enum

# 参考情報

* [MySQL: Fix schema dumping `enum` and `set` columns correctly](https://github.com/rails/rails/pull/36604)
* [MySQL: The ENUM Type](https://dev.mysql.com/doc/refman/8.0/en/enum.html)
* [MySQL: The SET Type](https://dev.mysql.com/doc/refman/8.0/en/set.html)