Rails 5 と MySQL 5.6 環境における utf8mb4 を扱う設定について

すでに Rails 環境における utf8mb4 の扱いに関しては多くの記事が出ているため新しいことは何も書いてありません。
Rails 5 と現状 MySQL 5.6 compatible な Aurora の環境での考察です。

## 環境

- Rails 5.0.1
- MySQL 5.6.22

### なぜ 5.6 か :question: 

Amazon Aurora を利用しており、まだ MySQL 5.7 compatible になっていないため 5.6 系である必要がありました。

### 今回利用する Charset と Collation 

Charset は `utf8mb4` に、Collation は `utf8mb4_bin` にすることとします。

Charset や Collation に関しては、直近で以下の素晴らしい資料があがっておりました。すごく詳しくてありがたい。
[MySQLの文字コード事情 2017版 - SlideShare](http://www.slideshare.net/tmtm/mysql-2017)

> ただ、今回はサーバー側も utf8mb4 で揃えるということはできないのですが。

## Rails の設定

今回私が気にした点は以下の 2 点のみです。

- `database.yml` の設定
- character を入れるカラムのインデックスに関して

### database.yml の設定

```yaml:database.yml
development:
  adapter: mysql2
  pool: 5
  username: root
  password:
  database: utf8mb4_sample_development
  charset: utf8mb4
  collation: utf8mb4_bin
  encoding: utf8mb4
```

以降では、ここで設定した `charset`, `collation`, `encoding` がどういう役割を担っていくかの一部を見ていきます。

#### charset と collation 設定

上記の `database.yml` に設定した状態で、`bin/rails db:create` を行うと以下のようになっています。

> character_set_database 以外はサーバー自体の設定が utf8 なので気にしないでください。 私が利用している Aurora の設定に合わせてあるだけです。

```sql
mysql> use utf8mb4_sample_development;
mysql> show variables like "chara%";
+--------------------------+------------------------------------------------------+
| Variable_name            | Value                                                |
+--------------------------+------------------------------------------------------+
| character_set_client     | utf8                                                 |
| character_set_connection | utf8                                                 |
| character_set_database   | utf8mb4                                              |
| character_set_filesystem | binary                                               |
| character_set_results    | utf8                                                 |
| character_set_server     | utf8                                                 |
| character_set_system     | utf8                                                 |
| character_sets_dir       | /usr/local/Cellar/mysql/5.6.22/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)

mysql> SHOW CREATE DATABASE utf8mb4_sample_development;
+----------------------------+------------------------------------------------------------------------------------------------------------+
| Database                   | Create Database                                                                                            |
+----------------------------+------------------------------------------------------------------------------------------------------------+
| utf8mb4_sample_development | CREATE DATABASE `utf8mb4_sample_development` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */ |
+----------------------------+------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```

デフォルトデータベースの `character_set_database` が `utf8mb4` になっており、 Database としてもデフォルトの charset と collate が狙い通りになっているのが分かります。

ではこの状態でテーブルを作ってみます。

```rb:migration/create_book.rb
class CreateBookTable < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.string :title

      t.timestamps
    end
  end
end
```

```shell-session
% bin/rails db:migrate
```

```rb:config/schema.rb
  create_table "book_tables", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin" do |t|
    t.string   "title"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
```

```sql
mysql> SHOW CREATE TABLE books;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| books | CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> show full columns from books;
+------------+--------------+-------------+------+-----+---------+----------------+---------------------------------+---------+
| Field      | Type         | Collation   | Null | Key | Default | Extra          | Privileges                      | Comment |
+------------+--------------+-------------+------+-----+---------+----------------+---------------------------------+---------+
| id         | int(11)      | NULL        | NO   | PRI | NULL    | auto_increment | select,insert,update,references |         |
| title      | varchar(255) | utf8mb4_bin | YES  |     | NULL    |                | select,insert,update,references |         |
| created_at | datetime     | NULL        | NO   |     | NULL    |                | select,insert,update,references |         |
| updated_at | datetime     | NULL        | NO   |     | NULL    |                | select,insert,update,references |         |
+------------+--------------+-------------+------+-----+---------+----------------+---------------------------------+---------+
4 rows in set (0.01 sec)
```

テーブル、カラムの設定も狙い通りの charset, collate になっていることが確認できています。

では試しに 4 byte 文字を入れてみます。

```sql
mysql> INSERT INTO books (title, created_at, updated_at) VALUES ('😇', NOW(), NOW());
Query OK, 1 row affected, 1 warning (0.01 sec)

mysql> SHOW warnings;
+---------+------+------------------------------------------------------------------------+
| Level   | Code | Message                                                                |
+---------+------+------------------------------------------------------------------------+
| Warning | 1366 | Incorrect string value: '\xF0\x9F\x98\x87' for column 'title' at row 1 |
+---------+------+------------------------------------------------------------------------+
1 row in set (0.00 sec)
```

お、Warning が出ていますね。 `Incorrect` となっています。中を見てみても以下のように正しく登録できていません。

```sql
mysql> SELECT * FROM books;
+----+-------+---------------------+---------------------+
| id | title | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 | ????  | 2017-02-02 17:00:39 | 2017-02-02 17:00:39 |
+----+-------+---------------------+---------------------+
```

最初に紹介したスライドにもあるようにクライアント側が `utf8` になっているためです。（かつ strict にはしていないので登録もされています）

なので、`utf8mb4` にします。

```sql
mysql> SET NAMES 'utf8mb4';
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like "chara%";
+--------------------------+------------------------------------------------------+
| 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     | utf8                                                 |
| character_set_system     | utf8                                                 |
| character_sets_dir       | /usr/local/Cellar/mysql/5.6.22/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)
```

```sql
mysql> SELECT * FROM books;
+----+-------+---------------------+---------------------+
| id | title | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 | ????  | 2017-02-02 17:00:39 | 2017-02-02 17:00:39 |
|  2 | 😇      | 2017-02-02 17:02:43 | 2017-02-02 17:02:43 |
+----+-------+---------------------+---------------------+
2 rows in set (0.00 sec)
```

無事登録できていますね :tada:

ついでにクライアントが `utf8` の場合には登録できた 4 byte 文字を正しく読めないことも確認しておきます。

```sql
mysql> SET NAMES 'utf8';
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like "chara%";
+--------------------------+------------------------------------------------------+
| Variable_name            | Value                                                |
+--------------------------+------------------------------------------------------+
| character_set_client     | utf8                                                 |
| character_set_connection | utf8                                                 |
| character_set_database   | utf8mb4                                              |
| character_set_filesystem | binary                                               |
| character_set_results    | utf8                                                 |
| character_set_server     | utf8                                                 |
| character_set_system     | utf8                                                 |
| character_sets_dir       | /usr/local/Cellar/mysql/5.6.22/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)
```

```sql
mysql> SELECT * FROM books;
+----+-------+---------------------+---------------------+
| id | title | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 | ????  | 2017-02-02 17:00:39 | 2017-02-02 17:00:39 |
|  2 | ?     | 2017-02-02 17:02:43 | 2017-02-02 17:02:43 |
+----+-------+---------------------+---------------------+
2 rows in set (0.00 sec)
```

読めていませんね。期待通りに動作しました。

#### SET NAMES がやっていること

先ほど `SET NAMES utf8mb4` を実行しましたが、挙動を見て分かる通り以下のような事をやっています。

https://dev.mysql.com/doc/refman/5.6/ja/charset-connection.html

> SET NAMES 'charset_name' [COLLATE 'collation_name']
>
SET NAMES は、クライアントからサーバーへの SQL ステートメントの送信に使用される文字セットを示します。したがって、SET NAMES 'cp1251' は、「このクライアントから今後受信するメッセージが文字セット cp1251 で送信される」ことを、サーバーに知らせます。また、クライアントに結果を返信するときにサーバーが使用する文字セットも指定します。(たとえば、SELECT ステートメントを使用する場合に、カラム値に使用する文字セットを指定します。)
>
SET NAMES 'charset_name' ステートメントは次の 3 つのステートメントと同等です。
>
SET character_set_client = charset_name;
SET character_set_results = charset_name;
SET character_set_connection = charset_name;


SET NAMES で変更される 3 つの変数は __クライアント側の文字セット (character set)__ に関するものであり、以下の説明がされています。

https://dev.mysql.com/doc/refman/5.6/ja/charset-connection.html

>
- クライアントから送信されるときに、ステートメントはどの文字セットで送信されますか。
>
サーバーは、character_set_client システム変数値を、クライアントが送信するステートメントの文字セットにします。
>
- ステートメントを受信したあとで、サーバーはこれをどの文字セットに変換しますか。
>
これには、サーバーは character_set_connection および collation_connection システム変数値を使用します。クライアントから送信されたステートメントは、character_set_client から character_set_connection に変換されます (_latin1 や _utf8 などのイントロデューサがある文字列リテラルを除きます)。collation_connection はリテラル文字列の比較で重要です。カラム値のある文字列の比較には、collation_connection は重要視されません。なぜなら、カラムには独自の照合順序があり、この照合順序が優先されるからです。
>
- 結果セットまたはエラーメッセージをクライアントに返送する前に、サーバーはこれらをどの文字セットに変換しますか。
>
character_set_results システム変数値は、サーバーがクライアントにクエリー結果を返信するときに使用する文字セットを示します。これには、カラム値などの結果データと、カラム名やエラーメッセージなどの結果メタデータが含まれます。

#### SET NAMES と encoding 設定

これは Rails では `encoding` の設定をすることによって有効にできます。

http://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html

> :encoding - (Optional) Sets the client encoding by executing “SET NAMES <encoding>” after connection.

実際のコードを見ると以下のようになっています。`collation` に関しても設定されていることがわかりますね。

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L887-L907

```rb:rails/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
        def configure_connection
          # ....

          # NAMES does not have an equals sign, see
          # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
          # (trailing comma because variable_assignments will always have content)
          if @config[:encoding]
            encoding = "NAMES #{@config[:encoding]}"
            encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
            encoding << ", "
          end

          # Gather up all of the SET variables...
          variable_assignments = variables.map do |k, v|
            if defaults.include?(v)
              "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
            elsif !v.nil?
              "@@SESSION.#{k} = #{quote(v)}"
            end
            # or else nil; compact to clear nils out
          end.compact.join(", ")

          # ...and send them all in one query
          execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
        end
```

よって、 `encoding` の設定が正しくしてあれば Rails から実行する場合には問題は起きません。

```shell-session
Loading development environment (Rails 5.0.1)
irb(main):001:0> Book.create(title: '😖')
   (0.2ms)  BEGIN
  SQL (3.8ms)  INSERT INTO `books` (`title`, `created_at`, `updated_at`) VALUES ('😖', '2017-02-02 08:22:47', '2017-02-02 08:22:47')
   (2.4ms)  COMMIT
=> #<Book id: 3, title: "😖", created_at: "2017-02-02 08:22:47", updated_at: "2017-02-02 08:22:47">
irb(main):004:0> b= Book.last
  Book Load (13.8ms)  SELECT  `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1
=> #<Book id: 3, title: "😖", created_at: "2017-02-02 08:22:47", updated_at: "2017-02-02 08:22:47">
irb(main):005:0> b.title
=> "😖"
```

#### database.yml の設定まとめ

ここまでで `database.yml` に指定した `charset`, `collation`, `encoding` がどのような役割をしたかを確認しました。

ひとまずはこの設定があれば `utf8mb4` 対応はできたことになります。

次に、だいたいぶつかるインデックスの課題について見ていきます。

### character を入れるカラムのインデックスに関して

[MySQL(InnoDB) で "Index column size too large. The maximum column size is 767 bytes." いわれるときの対策 - かみぽわーる](http://blog.kamipo.net/entry/2012/11/13/102024)

上記の記事から引用しますと、

> ActiveRecordでstring型のカラムを定義すると、MySQLだとvarchar(255)になるので、utf8mb4だとインデックス張ると767バイトを超えてしまう。

という自体が発生します。

これに関しては回避方法は多くあると思いますが、ここでは以下の 3 つに絞って考察します。

1. キープレフィックスの制限を拡張
2. カラムの文字数を制限
3. カラムの charset を変更

#### :one: キープレフィックスの制限を拡張

これに関しては、上記の記事でも解決法としてあげられています。

では Aurora 環境ではどうすればよいかですが、Aurora でも同様の事が実現可能と思われます。（というのも、実際やってはいない。。）
パラメータ的には可能だろうというものです。

- innodb_file_format
  - パラメータグループからオンラインで変更可能
- innodb_large_prefix
  - パラメータグループからオンラインで変更可能
- innodb_file_per_table
  - パラメータグループには無いが、設定を見ると元から 1 になっている
- ROW_FORMAT
  - Rails の `create_table` オプションに渡す (5.6 系なので）

```rb:db/migrate/create_book.rb
create_table :books, options: 'ROW_FORMAT=DYNAMIC' do |t|
```

```rb:db/schema.rb
  create_table "books", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC" do |t|
```

file_format に関しては以下の記事が勉強になりました。
[InnoDBの制限とファイルフォーマットAntelopeとBarracudaの違い - かみぽわーる](http://blog.kamipo.net/entry/2014/12/05/235641)

Rails での ROW_FORMAT に関しては以下が参考になります。

- [Rails(ActiveRecord)とMySQLでutf8mb4を扱う設定 - Qiita](http://qiita.com/xend/items/4d5c3333cbae53888f37)
- [ActiveRecordをutf8mb4で動かす - Qiita](http://qiita.com/kamipo/items/101aaf8159cf1470d823)

#### :two: カラムの文字数を制限

インデックスに使うカラムを 191 文字以下に制限するという方法です。

一時期は `schema_migrations` テーブルの `version` カラムに関してもこの対応が取られていたのでしょうか。
https://github.com/rails/rails/pull/17601

- [Rails4 で MySQL の utf8mb4 を扱う - xykのブログ](http://xyk.hatenablog.com/entry/2014/10/03/021123)
- [Mysqlデータベースに絵文字を格納する方法 - Qiita](http://qiita.com/dainghiavotinh/items/eaf68301d1cd109fae83)
- [How to store emoji in a Rails app with a MySQL database](http://blog.arkency.com/2015/05/how-to-store-emoji-in-a-rails-app-with-a-mysql-database/)

最も単純にやるなら以下の方法で良さそうです。

```rb:db/migrate/create_book.rb
class CreateBook < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.string :title, limit: 191, index: true

      t.timestamps
    end
  end
end
```

```rb:db/schema.rb
  create_table "books", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin" do |t|
    t.string   "title",      limit: 191
    t.datetime "created_at",             null: false
    t.datetime "updated_at",             null: false
    t.index ["title"], name: "index_books_on_title", using: :btree
  end
```

#### :three: カラムの charset を変更

4byte の charset でなければ良いので例えばそのカラムが utf8 でも問題無いのなら、カラムの charset を変更すれば文字数の最大値を減らさなくてもすみます。

:bulb: 当然ではありますが、charset を変えるということはインデックスを付けるカラムには 4byte 文字は入らなくなるのでその点は注意してください。

方法に関してですが以下のように `charset` を直接指定することもできます。

```rb
class CreateBook < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.string :title, index: true, charset: :utf8

      t.timestamps
    end
  end
end
```

これで charset 自体は確かに問題なくインデックスも作成されるのですが、以下のように `collate` が意図しないものに変わります。

```sql
mysql> show full columns from books;
+------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| Field      | Type         | Collation       | Null | Key | Default | Extra          | Privileges                      | Comment |
+------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| id         | int(11)      | NULL            | NO   | PRI | NULL    | auto_increment | select,insert,update,references |         |
| title      | varchar(255) | utf8_general_ci | YES  | MUL | NULL    |                | select,insert,update,references |         |
| created_at | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
| updated_at | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
+------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
4 rows in set (0.00 sec)
```

ここに関しては Rails が `schema_migrations` に対して現在取っているアプローチが良さそうです。

https://github.com/rails/rails/blob/65bf1c60053e727835e06392d27a2fb49665484c/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L75-L81
(PR はこれかな？ https://github.com/rails/rails/pull/23168)

```rb:rails/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
      #...

      CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]

      def internal_string_options_for_primary_key # :nodoc:
        super.tap { |options|
          options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
        }
      end
```

これは `collate` の方を直しています。例えば `utf8mb4_bin` であったのなら `utf8_bin` になります。

この方法を用いると以下のように書けます。

```rb
class CreateBook < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.string :title, index: true, collation: :utf8_bin

      t.timestamps
    end
  end
end
```

これを実行すると以下のような結果になります。

```rb:db/schema.rb
  create_table "books", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin" do |t|
    t.string   "title",                   collation: "utf8_bin"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["title"], name: "index_books_on_title", using: :btree
  end
```

```sql
mysql> show full columns from books;
+------------+--------------+-----------+------+-----+---------+----------------+---------------------------------+---------+
| Field      | Type         | Collation | Null | Key | Default | Extra          | Privileges                      | Comment |
+------------+--------------+-----------+------+-----+---------+----------------+---------------------------------+---------+
| id         | int(11)      | NULL      | NO   | PRI | NULL    | auto_increment | select,insert,update,references |         |
| title      | varchar(255) | utf8_bin  | YES  | MUL | NULL    |                | select,insert,update,references |         |
| created_at | datetime     | NULL      | NO   |     | NULL    |                | select,insert,update,references |         |
| updated_at | datetime     | NULL      | NO   |     | NULL    |                | select,insert,update,references |         |
+------------+--------------+-----------+------+-----+---------+----------------+---------------------------------+---------+
4 rows in set (0.01 sec)

mysql> SHOW CREATE TABLE books;
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                                         |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| books | CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_books_on_title` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```

charset が `utf8` になり collate も意図通りになりました。

#### character を入れるカラムのインデックスに関してのまとめ

4byte の character を扱うカラムのインデックスに関して、回避方法 3 つを調査しました。

1 で対応できるならば 1 が最もコードへの影響はなく良さそうに見えます。
2, 3 に関しては、最大文字長・charset の制限に対して要件として問題無いのであれば、選択しても良さそうです。

> 私個人としては DBA とも相談し、要件を鑑みた上で 2 で十分という事になり 2 を採用しました。

## 余談

### ActiveRecord::ValueTooLong に関して

[Rails5から使えるActiveRecord便利機能 - Qiita](http://qiita.com/mitz/items/b2295e440585ba63b083#mysql%E3%81%A8postgresql%E3%81%A7%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%AE%E5%88%B6%E9%99%90%E5%80%A4%E3%82%92%E8%B6%85%E3%81%88%E3%81%A6%E3%81%84%E3%82%8B%E3%81%A8%E3%81%8Dactiverecordvaluetoolong%E3%82%92raise%E3%81%99%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB) の記事で知ったのですが、 `ActiveRecord::ValueTooLong` が制限値を超えているケースで `raise` されるようになったようです。

同僚が昔に MySQL で制限値を超えた時に文字を切られて保存されたという事象にあたったと聞いたので、今回 limit を付けることもあり調査していました。

https://github.com/rails/rails/blob/fdef4b7baa7a5374058e3b97697d9531b57265c1/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L733-L769

```rb
module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
      # ...

      private

        # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
        ER_DATA_TOO_LONG        = 1406

        def translate_exception(exception, message)
          case error_number(exception)
          when ER_DUP_ENTRY
            RecordNotUnique.new(message)
          # 略
          when ER_DATA_TOO_LONG
            ValueTooLong.new(message)
          when ER_OUT_OF_RANGE
            RangeError.new(message)
          # 略
          else
            super
          end
        end
```

上記の通り、この例外は MySQL の `ER_DATA_TOO_LONG` というエラーコードが投げられた時に変換されるもののようです。

[B.3 Server Error Codes and Messages - MySQL](https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html)

>Error: 1406 SQLSTATE: 22001 (ER_DATA_TOO_LONG)
>
Message: Data too long for column '%s' at row %ld

このエラーは sql_mode が strict になっていなければ発行されません。 (`STRICT_TRANS_TABLES`, `STRICT_ALL_TABLES`, `TRADITIONAL`)
[5.1.7 サーバー SQL モード - MySQL](https://dev.mysql.com/doc/refman/5.6/ja/sql-mode.html)

ここまで見てきたように私が検証していた DB は strict ではありません。（Warning を出して不正な文字が登録されていた）

しかし Rails console から文字数制限を超える処理を実行すると `ActiveRecord::ValueTooLong` は発行されてきます。

#### strict が未指定の場合は true

以下は encoding の時も出てきた `#configure_connection` です。

https://github.com/rails/rails/blob/65bf1c60053e727835e06392d27a2fb49665484c/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L878-L893

```rb:rails/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter

      def strict_mode?
        self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
      end

      private

        def configure_connection
          # ....

          # Make MySQL reject illegal values rather than truncating or blanking them, see
          # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
          # If the user has provided another value for sql_mode, don't replace it.
          if sql_mode = variables.delete("sql_mode")
            sql_mode = quote(sql_mode)
          elsif !defaults.include?(strict_mode?)
            if strict_mode?
              sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')"
            else
              sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')"
              sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')"
              sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')"
            end
            sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
          end
          sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode
```

上記を見て分かる通り、 `strict_mode?` が `true` の場合には `STRICT_ALL_TABLES` が `sql_mode` に指定されることがわかります。

そして `#strict_mode?` を見ると、 `@config` 今回だと `database.yml` の設定から `strict` を fetch する際にデフォルトを `true` にしています。

実際に以下のように `true` が返ってくることがわかります。

```rb
irb(main):007:0> Book.connection.strict_mode?
=> true
```

よって、明示的に `strict` や `sql_mode` を異なるものに指定しない限りは `STRICT_ALL_TABLES` が適用されているため、データベースにその設定をしていなくても Rails での実行時には `ActiveRecord::ValueTooLong`を受け取れるということになります。