Rails (ActiveRecord) マイグレーションで MySQL テーブル作成時の文字コード (DEFAULT CHARSET) はどのように設定されるのか

# はじめに結論
`db:create` 実行時に、config/database.yml の `encoding` 設定に基づき、データベースレベルで設定される。

なので、Rails (ActiveRecord) のマイグレーションでは以下に留意する。

- config/database.yml に `encoding` 設定があるか？
  - この設定が存在しない場合、`db:migrate` で作成されるテーブルの `DEFAULT CHARSET` が意図しないものになる可能性がある（Rails 5.1.4 では `utf8` になる）
- データベースが `db:create` によって作成されているか？
  - 他手段（SQL直実行など）で作成した場合、`db:migrate` で作成されるテーブルの `DEFAULT CHARSET` が config/database.yml の `encoding` 設定の値と一致しなくなる
- `db:create` 実行後に config/database.yml の `encoding` を変えたりしていないか？
  - この状態で `db:drop db:create` せずに `db:migrate` を実行すると、新規作成されるテーブルの `DEFAULT CHARSET` は 以前の `encoding` の設定値になる

あるいは、以下のようにマイグレーションスクリプト側で `DEFAULT CHARSET` を含むオプションを明記するアプローチもある。こうすれば、上述の留意点が不要になる。

```ruby:db/migrate/yyyymmddhhmmss_create_examples.rb
class CreateExamples < ActiveRecord::Migration[5.1]
  def change
    create_table :examples, options: "DEFAULT CHARSET=utf8mb4" do |t|
      ...
    end
  end
end
```

# どういうことなのか
MySQLでは、テーブル作成時の `DEFAULT CHARSET` が以下の2段階で定義される。

- [character_set_server](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_character_set_server)
- [character_set_database](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_character_set_database)

上記のうち、`character_set_database` はデータベース毎に異なる値を持つことができる。各データベースで何が設定されているかは、例えば以下の方法で確認できる。

- `SHOW VARIABLES` 構文：

    ```sql:
    mysql> USE some_database;
    Database changed
 
    mysql> SHOW VARIABLES LIKE 'character_set_database'; -- 現在選択中のデータベースによって値が変わる
    +------------------------+---------+
    | Variable_name          | Value   |
    +------------------------+---------+
    | character_set_database | utf8mb4 |
    +------------------------+---------+
   ```

- `SHOW CREATE DATABASE` 構文：

    ```sql:
    mysql> SHOW CREATE DATABASE some_database;
    +---------------+---------------------------------------------------------------------------+
    | Database      | Create Database                                                           |
    +---------------+---------------------------------------------------------------------------+
    | some_database | CREATE DATABASE `some_database` /*!40100 DEFAULT CHARACTER SET utf8mb4 */ |
    +---------------+---------------------------------------------------------------------------+
    ```

この `character_set_database` に相当する値は、データベース作成時に以下のようにして設定できる。

```sql:
mysql> CREATE DATABASE some_database DEFAULT CHARACTER SET utf8mb4;
Query OK, 1 row affected (0.00 sec)
```

このデータベースに対して `DEFAULT CHARSET` オプションの無いテーブルを作成した場合、`DEFAULT CHARSET` には `character_set_database` の値が設定される。

```sql:
mysql> CREATE TABLE examples (id bigint AUTO_INCREMENT PRIMARY KEY);
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW CREATE TABLE examples\G
*************************** 1. row ***************************
       Table: examples
Create Table: CREATE TABLE `examples` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
```

そして Rails (ActiveRecord) マイグレーションでは、`db:create` 実行時に config/database.yml の `encoding` 設定に基づき `character_set_database` 相当の値を設定するように実装されているようだ。更に、`encoding` 設定が無い場合は `utf8` を設定するようだ。

- [activerecord-5.1.4: lib/active_record/tasks/mysql_database_tasks.rb#L12](https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/tasks/mysql_database_tasks.rb#L12)

    ```ruby:
    def create
      establish_connection configuration_without_database
      connection.create_database configuration["database"], creation_options
      ...
    ```
- [activerecord-5.1.4: lib/active_record/tasks/mysql_database_tasks.rb#L87](https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/tasks/mysql_database_tasks.rb#L87)

    ```ruby:
    def creation_options
      Hash.new.tap do |options|
        options[:charset]     = configuration["encoding"]   if configuration.include? "encoding"
        options[:collation]   = configuration["collation"]  if configuration.include? "collation"
      end
    end
    ````

- [activerecord-5.1.4: lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L277](https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L277)

    ```ruby:
    def create_database(name, options = {})
      if options[:collation]
        execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
      else
        execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
      end
    end
    ```

上記は 5.1.4 (2018/01/17 時点での最新版) での実装だが、例えば [Qiitaのこのエントリ](https://qiita.com/aosho235/items/bca4a2117b78baa927d7#mysql) を見ると 4.2系でも同様の実装のようだ。

# どのようにハマり得るのか
例えば、事前にSQL直実行でデータベースを作成し（この時点で作成したデータベースの `character_set_database` はMySQLサーバ設定依存の `latin1` とかになり得る）、それから `db:migrate` を実行した場合、database.yml の `encoding` が `utf8mb4` になっていても、作成されるテーブルの `DEFAULT CHARSET` は `latin1` とかになってしまう。

また例えば、過去に database.yml の `encoding` を 未指定 or `utf8` にした状態で `db:create` していて、その後 `encoding` を `utf8mb4` に変更してから `db:migrate` を実行した場合、新たに作成されるテーブルの `DEFAULT CHARSET` は `utf8` のままになる。

この際タチが悪いのは、`db:migrate` によって作成/更新される db/schema.rb のテーブル定義の `DEFAULT CHARSET` は、database.yml の `encoding` 設定に基づいた値になっているということ（その値でテーブルを作成した気になっている）。このため、db/schema.rb に `DEFAULT CHARSET=utf8mb4` とあるので安心していたが実際のテーブルは `latin1` とか `utf8` になっていた、という事態が発生し得る。

いずれの場合も、一旦 `db:drop db:create` してから `db:migrate` することで、現在の database.yml の `encoding` の値を新規作成テーブルの `DEFAULT CHARSET` に反映できる。


# どう気をつけると良いのか
「はじめに結論」の再掲だが、Rails (ActiveRecord) のマイグレーションでは以下に気をつける。

- config/database.yml に `encoding` 設定があるか？
  - この設定が存在しない場合、`db:migrate` で作成されるテーブルの `DEFAULT CHARSET` が意図しないものになる可能性がある（Rails 5.1.4 では `utf8` になる）
- データベースが `db:create` によって作成されているか？
  - 他手段（SQL直実行など）で作成した場合、`db:migrate` で作成されるテーブルの `DEFAULT CHARSET` が config/database.yml の `encoding` 設定の値と一致しなくなる
- `db:create` 実行後に config/database.yml の `encoding` を変えたりしていないか？
  - この状態で `db:drop db:create` せずに `db:migrate` を実行すると、新規作成されるテーブルの `DEFAULT CHARSET` は 以前の `encoding` の設定値になる

あるいは、以下のようにマイグレーションスクリプト側で `DEFAULT CHARSET` を含むオプションを明記するアプローチもある。こうすれば、上述の留意点が不要になる。

```ruby:db/migrate/yyyymmddhhmmss_create_examples.rb
class CreateExamples < ActiveRecord::Migration[5.1]
  def change
    create_table :examples, options: "DEFAULT CHARSET=utf8mb4" do |t|
      ...
    end
  end
end
```