【Rails×MySQL】utf8mb4対応の"encoding"と"charset"の、それぞれの役割について調べた

rails×MySQLについて、"rails utf8mb4"とかで検索すると、ほとんどの記事でdatabase.ymlにencodingとcharsetを両方書いています。

```database.yml
# ... 略
 encoding: utf8mb4
 charset: utf8mb4
# ... 略
```

encodingとcharsetは同じ値を設定していますが、それぞれの役割が分からないので、動作検証とソース確認をしてみました。

## 結論
・charsetはどこにも使われていない（ように見える）
・encodingはデータベースおよびシステム変数のcharset関連に適用される（結果としてテーブル・カラムも同様のcharsetになる）

## 検証環境
Rails: 7.0.4.2
MySQL: 8.0.32
mysql2: 0.5.5

## そもそもMySQLでCharset指定できる箇所
MySQLでCharset関連の指定ができる箇所は、以下の４つがあるかと思います。
1. データベース
（例：create database test_database **charset utf8mb4**;）
2. テーブル
 （例：create table test_table (name varchar(255)) **charset utf8mb4**;）
3. カラム
（例：alter table test_table add test_column text **charset utf8mb4**;）
4. システム変数
（例1： set **<span>@@</span>character_set_server="utf8mb4"**;）

今回はこの４つについて、動作検証とソース確認を行なっていきます。

## 動作検証1 encodingを変えた場合
まずはencodingを変えた際にどうなるかを確認していきます。
変える値は、みんな大好きlatin1君です。
他方はコメントアウトしておきます。
```database.yml
  encoding: latin1
  #charset: latin1
  database: rails_7_encoding
```

これで、encodingが影響する箇所はlatin1に、しない箇所はMySQL8のデフォルトであるutf8mb4になるはずです。

### 1. データベース作成時
```
rails db:create
```
↓
```
mysql> show create database rails_7_encoding\G
*************************** 1. row ***************************
       Database: rails_7_encoding
Create Database: CREATE DATABASE `rails_7_encoding` 
/*!40100 DEFAULT CHARACTER SET latin1 */ /*!80016 DEFAULT ENCRYPTION='N' */
```
latin1になっています。

### 2. テーブル作成時　と　3. カラム作成時
```create.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end
  end
end
```
↓
```
mysql> show create table users\G
*************************** 1. row ***************************
       Table: users
Create Table: CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> select column_name, character_set_name from information_schema.columns where table_schema="rails_7_encoding" and table_name="users";
+-------------+--------------------+
| COLUMN_NAME | CHARACTER_SET_NAME |
+-------------+--------------------+
| created_at  | NULL               |
| id          | NULL               |
| name        | latin1             |
| updated_at  | NULL               |
+-------------+--------------------+
```
どちらもlatin1になっています。

### 4. システム変数
```
rails dbconsole
```
↓
```
mysql> show variables like "%character%";
+--------------------------+---------------------------------------------------------+
| Variable_name            | Value                                                   |
+--------------------------+---------------------------------------------------------+
| character_set_client     | latin1                                                  |
| character_set_connection | latin1                                                  |
| character_set_database   | latin1                                                  |
| character_set_filesystem | binary                                                  |
| character_set_results    | latin1                                                  |
| character_set_server     | utf8mb4                                                 |
| character_set_system     | utf8mb3                                                 |
| character_sets_dir       | /opt/homebrew/Cellar/mysql/8.0.32/share/mysql/charsets/ |
+--------------------------+---------------------------------------------------------+
```
latin1になっています。（character_set_server, character_set_systemは無視して構いません）
というわけで、encodingは全ての箇所で適用されました。
（後述しますが、厳密にはテーブルとカラムはChasetを明示的に指定しているわけではなく、データベースと一緒になるだけです）

## 動作検証2 charsetを変えた場合
反対もやってみます。
```database.yml
  # encoding: latin1
  charset: latin1
  database: rails_7_charset
```
手順は同じなので、結果だけ貼ります。

### 1. データベース作成時
```
mysql> show create database rails_7_charset\G
*************************** 1. row ***************************
       Database: rails_7_charset
Create Database: CREATE DATABASE `rails_7_charset` 
/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */
```
latin1は反映されていません。

### 2. テーブル作成時　と　3. カラム作成時
```
mysql> show create table users\G
*************************** 1. row ***************************
       Table: users
Create Table: CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

mysql> select column_name, character_set_name from information_schema.columns where table_schema="rails_7_charset" and table_name="users";
+-------------+--------------------+
| COLUMN_NAME | CHARACTER_SET_NAME |
+-------------+--------------------+
| created_at  | NULL               |
| id          | NULL               |
| name        | utf8mb4            |
| updated_at  | NULL               |
+-------------+--------------------+
```
どちらもlatin1は反映されていません。

### 4. システム変数
```
mysql> show variables like "%character%";
+--------------------------+---------------------------------------------------------+
| Variable_name            | Value                                                   |
+--------------------------+---------------------------------------------------------+
| character_set_client     | utf8mb4                                                 |
| character_set_connection | utf8mb4                                                 |
| character_set_database   | utf8mb4                                                 |
| character_set_filesystem | binary                                                  |
| character_set_results    | utf8mb4                                                 |
| character_set_server     | utf8mb4                                                 |
| character_set_system     | utf8mb3                                                 |
| character_sets_dir       | /opt/homebrew/Cellar/mysql/8.0.32/share/mysql/charsets/ |
+--------------------------+---------------------------------------------------------+
```
latin1は反映されていません。

全て無視されました。



・・・・・・🤔


## ソースコード確認
mysql2とrailsのソースを確認してきます。
ここからは、元々の目的である”encodingとcharsetのそれぞれの役割を調べる”というより、
・encodingの役割を調べる
・ついでにcharsetが使われている場所が見つかったらラッキー
という感じで見ています。

### mysql2での接続時の設定
mysql2のgemで接続する際の挙動を見てみます。
コネクションを張る時の動きです。
```/lib/mysql2/client.rb
def initialize(opts = {})
      raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash

      opts = Mysql2::Util.key_hash_as_symbols(opts)

      # ...略

      # force the encoding to utf8
      self.charset_name = opts[:encoding] || 'utf8'
```
optsにはdatabase.ymlに書いた内容が入っています。
encodingが使われているのが分かります。

このあとはCファイル内でAPI関数を呼び出し、charsetを設定しているように見えます。（C言語全く分かりません、雰囲気で見ただけです）

```/ext/mysql2/client.c
static VALUE set_charset_name(VALUE self, VALUE value) {
  /* ...略 */
  if (mysql_options(wrapper->client, MYSQL_SET_CHARSET_NAME, charset_name)) {
    /* TODO: warning - unable to set charset */
    rb_warn("%s\n", mysql_error(wrapper->client));
  }
```

mysql_options
https://dev.mysql.com/doc/c-api/8.0/en/mysql-options.html

### SETでのシステム変数設定
接続した後はSET文でシステム変数を変えています。

```activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
def configure_connection
  # ...略
  if @config[:encoding]
    encoding = +"NAMES #{@config[:encoding]}"
    encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
    encoding << ", "
  end
  # ...略
  internal_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}")
```
`@config`にはdatabase.ymlに書いた内容が入っています。
encodingが使われているのが分かります。
"SET NAMES latin1;"といった形になるのが分かります。

SET NAMESについて[MySQLの公式](https://dev.mysql.com/doc/refman/8.0/ja/set-names.html)から引用すると
> このステートメントは、character_set_client、character_set_connection および character_set_results の 3 つのセッションシステム変数を特定の文字セットに設定します。

とあるため、encodingの動作検証の結果と一致します。
（ちなみにcharacter_set_databaseも変わっていたのは、データベースがlatin1のcharsetとなっているためです。）

このSETはmysql2内で設定した内容と事実上かぶっているようにも見えるんですが、どうなんでしょうか。

### データベース作成時
続いてデータベース・テーブル・カラム作成時のSQL発行時のCharsetについて確認します。
まずはデータベースから
```active_record/connection_adapters/abstract_mysql_adapter.rb
  def create_database(name, options = {})
    if options[:collation]
      execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
    elsif options[:charset]
      execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}"
    elsif row_format_dynamic_by_default?
      execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`"
    else
      raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
    end
  end
```
！！！！！！
options[:charset]！！options[:charset]がありました！！！！！

・・・残念ながらencodingを見ているだけです。
```
def creation_options
  Hash.new.tap do |options|
    options[:charset]     = configuration_hash[:encoding]   if configuration_hash.include?(:encoding)
    options[:collation]   = configuration_hash[:collation]  if configuration_hash.include?(:collation)
  end
end
```
「これって逆もあり得るのでは？今まで見てきた"config[:encoding]"みたいなのも、実は途中でcharsetの値になる処理がどこかにあったんじゃない？」

もっともなご指摘です。調査をお願いします。
（一応、それなりに順に追って見ているのと、"charset"で検索とかはしています。）

### テーブル作成時
https://github.com/rails/rails/blob/73255bae9e2eb1494b01d120530eab6d0b371646/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb#L42-L42
###  カラム作成時
https://github.com/rails/rails/blob/73255bae9e2eb1494b01d120530eab6d0b371646/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb#L38-L38

体力尽きてしまい詳しく見れていませんが、テーブル作成・カラム作成時にdatabase.ymlの中を見ることはなさそうでした。
結果として、テーブルとカラムはデータベースのCharsetと同じものになります。

## おしまいに
というわけで、長々と書いてしまいましたが、最初に書いた[結論](#結論)の通りとなります。
謎の設定charset君が、結局謎のままなのが悲しいところです。
こいつがいなければ「encodingについて調べてみた！！」みたいなシンプルな内容にできたはずです。
railsとmysql2について古いバージョンもチラ見しましたが、どれもencodingしか見ていないように見えました。（本当にチラ見程度です）
もしかして、昔からずっとゴミが混ざっているだけの可能性が・・・？
もし使われている箇所や、経緯などをご存知の方がいたらコメント頂けるととても嬉しいです。