【Mysql2】Mysql2::Error: Specified key was too long; max key length is 767 bytes→文字制限をかけるファイルを新規作成することで解決する


#ざっくり概要について#

このエラーは「Mysql2に、格納できる文字データは、767バイトまでだよ！今のままでは、Mysqlにマイグレーションできないよ！」という意味で理解しました。

そのエラーを踏まえて、この記事では、以下２つの方法を実践しました。

- [database.ymlが「utf8mb4」のまま、「文字制限をかけるファイルを新規作成」する方法](https://qiita.com/terufumi1122/items/9ea764618eba01144e09)
（@terufumi1122さんの記事のおかげで解決できました）
- 「文字制限をかける新規ファイル」を作成せずに、database.ymlを「utf８」に書き直す方法（この方法はやらないほうがいいと学びました）

その上で一番大事だなと思ったことは、 MySQLを使用する際、
「絵文字を使えるかどうか、要件定義の際にしっかり決める」ということです。

では、詳細にいきましょう！！！

#エラー内容について#

「Mysql2::Error: Specified key was too long; max key length is 767 bytes」というエラーが発生。

```terminal:terminal

〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:reset                          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0174s
-- add_index(:users, :email, {:unique=>true})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Specified key was too long; max key length is 767 bytes
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:39:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate:reset => db:migrate
(See full trace by running task with --trace)

```

#エラーが発生した状況について#

#####環境#####

- Rails 6.0.0を使用
- データベースはMysql2を使用（アプリケーションはSequel Pro）
- deviseというgemを使用（そのためモデルは、rails g devise userで作成していた）
- deviseでは、マイグレーションファイルにおいて、string型（例：t.string :email）にあたるカラムを作成しようとしていた
- マイグレーションファイルを作成し、rails db:migrate を実行したところ、エラー発生

#####初めは状況を理解できず、下記のことをさらに行ってしまった#####

- rails db:migrate:statusで確認　→　downになっている
-  再度、rails db:migrate　→　エラー
-  rails db:migrate:reset →　エラー　※上記はこの画面！


#解決方法に行く前に、そもそも文字コードって何？「utf8」と「utf8mb4」って何？byteって何？#

その概念が分からず、自分なりにですが、以下のように理解しました。

- 文字コードとは、「あ」だったら１番、「い」だったら2番というように、文字に対して割り振っているコードのこと。（実際には進数など使って、もっと複雑そうですが・・・）

- 「utf8」とは文字コードの中でも、世界で最も普及している文字コード。「utf8」は1～4バイトで文字を表現するが、MySQLでは3バイトの文字までしか扱えない。

- 「utf8mb4」とは、データベースMySQLで扱うための文字コード。Mysqlにおいて、絵文字などは「utf8」の4バイトに当たるので、「utf8mb4」でないと絵文字が扱えない。

- MySQLにおいて、保存できる文字数は「255文字」まで。

####こちらから引用させていただきました###

- [「utf8」と「utf8mb4」の違いについて](https://yokonoji.work/utf-8mb4)
- [文字コードとは](https://gihyo.jp/book/pickup/2019/0006)
- [単一カラムインデックスの最大キー長は767バイトまで](https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0032)
-  [MysqlにおけるVARCHAR型とは](https://dev.mysql.com/doc/refman/5.6/ja/char.html)
-  [Railsにおけるstringのデフォルト値２５５文字まで](https://chinatz.hatenablog.com/entry/2018/07/20/210000)


#上記を組み合わせて、理解したこと#

つまり、MySQLにおいて、

- utf8は文字を扱う文字コード、utf8bm4は絵文字を扱う文字コード

可能な文字数は、

- 「utf8」は「３バイト」使うので、「７６７バイト➗３バイト=２５５文字まで」

- 「utf8mb4」は「4バイト」使うので、「767バイト➗４バイト=１９１文字まで」

stringにおけるデフォルト値が255文字なので、

- 「３バイト❌２５５文字=765バイト」（utf8）

- 「４バイト❌２５５文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ！！:point_right_tone3:今回はこの状態に陥っており、エラーになっている

ということが、わかりました。


#その上で、エラー解決方法へ#

さて、エラー内容が起きた原因がすっきりしたところで、肝心の解決方法についてです。

冒頭で触れた通り、

- [database.ymlが「utf8mb4」のまま、「文字制限をかけるファイルを新規作成」する方法](https://qiita.com/terufumi1122/items/9ea764618eba01144e09)
（@terufumi1122さんの記事）

まずは、上記方法で、解決しました！！！（ありがとうございます）

解決方法について、概要を話すと、

【問題点】
「４バイト❌２５５文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ！！:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、７６７byte超えないように、（「4バイト❌１９１文字=７６４バイト」）文字数の上限を１９１文字に設定しようよ！という方法です。（絵文字が使えるように、utf8bm4はそのまま活かす方法）


>以下のようなmysql.rbをconfig/initializer配下に新規作成します。(以下のコード含め、@terufumi1122さんの記事から引用しています。)

```ruby:config/initializer/mysql.rb
require 'active_record/connection_adapters/abstract_mysql_adapter'

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter
      NATIVE_DATABASE_TYPES[:string] = { :name => "varchar", :limit => 191 }
    end
  end
end
```

上記ファイルを入力した後に、

```terminal:terminal
rails db:migrate
```

をしたところ、無事にマイグレートされました！！！
（MySQLであるSequelProを見たところ、カラムができていました。）

【2021/1/12追記】
もし、マイグレートした際`StandardError: An error has occurred, all later migrations canceled:`とエラーが出たら・・・
1. `rails db:migrate:status`で`down`になっているか確認
2. `rails db:reset `の実行
3. `rails db:migrate`の再実行で上手くいった
<br>
<br>

#そこから、考察したこと#

さて、無事に解決したのですが、問題点に対して、７６７byteを超えない方法について、下記のような考え方もできないかなと思いました。

【問題点】
「４バイト❌２５５文字=1020バイト」(utf8bm4):point_right_tone3:MySQLで保存できる767byte超えてるよ！！:point_right_tone3:今回はこの状態に陥っており、エラーになっている

【それに対する考え方】
じゃあ、もし、そのアプリケーションで絵文字を使わないのであれば、「３バイト❌２５５文字=765バイト」（utf8）に変更するのはどうか。つまり、１９１文字以下にするファイルを作成するのではなく、「config>database.yml」に記載している、文字コードを、「utf8bm4」から「utf8」に書き換えて、マイグレーションすればいいのでは！？

と思いました。

ここから実践する訳ですが、結論から言うと、「モデルから作り直しになる（マイグレーションの書き換えでは済まない）」ということを学びました。（つまり、面倒で大変です。）

「マイグレーションの書き換えで済まない」とは、「rails db:migrate」と共に、（指示をしていない）謎のマイグレーションファイルが出現し、そのマイグレーションファイルは不要なので、架空ファイルに変更して、ドロップやリセットを行いますが消そうとしても消えません。

「rails d devise user」でいったんモデルを消して、「rails g devise user」で作り直してやっと、エラーの出ない「utf8」文字コードのアプリケーションができました。

####いざ、実践！（先ほどのエラー解決した状況から、上記考察を実践しています）#####


######①いったんロールバック######
```terminal:terminal

〇〇@〇〇noMacBook-Air devise_app % rails db:rollback       
== 20200925212154 DeviseCreateUsers: reverting ================================
-- remove_index(:users, {:column=>:reset_password_token})
   -> 0.0099s
-- remove_index(:users, {:column=>:email})
   -> 0.0074s
-- drop_table(:users)
   -> 0.0040s
== 20200925212154 DeviseCreateUsers: reverted (0.0250s) =======================
```


######②マイグレーションの状況を確認 (downになっているので、よし！)######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20200925212154  Devise create users

```


######③マイグレーションを実行（エラーを再現できたため、よし！）######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200925212154 DeviseCreateUsers: migrating ================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'

Caused by:
Mysql2::Error: Table 'users' already exists
/Users/〇〇/projects/devise_app/db/migrate/20200925212154_devise_create_users.rb:5:in `change'
/Users/〇〇/projects/devise_app/bin/rails:9:in `<top (required)>'
/Users/〇〇/projects/devise_app/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
```

######④config>database.ymlをutf8に書き直す######

```ruby:config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock
```

######⑤rails db:resetを実行（良い感じ！）######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:reset  
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
```

######⑥rails db:migrateを実行（良い感じ！）######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate
== 20200925212154 DeviseCreateUsers: migrarails db:migrateting ================================
-- create_table(:users)
   -> 0.0109s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0087s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0085s
== 20200925212154 DeviseCreateUsers: migrated (0.0283s) =======================
```

######⑥rails db:migrate:status（おや？　000のファイルってなんだ？）######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
   up     20200925212154  Devise create users
```

######⑦「＊＊NO　FILE＊」になってしまっているため、架空のファイル（Sample）にマイグレーションを修正して、ロールバックするも、０００ファイルがdownにならない######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:rollback STEP=2
〇〇@〇〇noMacBook-Air devise_app % 
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             Sample
  down    20200925212154  Devise create users
```

######⑦resetしてステータスを確認するも、０００ファイルがdownにならない######
```terminal:terminal
〇〇@〇〇noMacBook-Air devise_app % rails db:reset          
Dropped database 'devise_app_development'
Dropped database 'devise_app_test'
Created database 'devise_app_development'
Created database 'devise_app_test'
You have 1 pending migration:
  20200925212154 DeviseCreateUsers
Run `rails db:migrate` to update your database then try again.
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     000             ********** NO FILE **********
  down    20200925212154  Devise create users

```

######⑧モデルを削除する######
```terminal:terminal
〇〇noMacBook-Air devise_app % rails d devise user   
Running via Spring preloader in process 10337
      invoke  active_record
      remove    db/migrate/20200925212154_devise_create_users.rb
      remove    app/models/user.rb
      invoke    test_unit
      remove      test/models/user_test.rb
      remove      test/fixtures/users.yml
       route  devise_for :users
```

######⑨モデルを作り直す######
```terminal:terminal
〇〇noMacBook-Air devise_app % rails g devise user    
Running via Spring preloader in process 10485
      invoke  active_record
      create    db/migrate/20200926073222_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users
```

######⑩マイグレーションを行う######
```terminal:terminal
〇〇noMacBook-Air devise_app % rails db:migrate        
== 20200926073222 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0275s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0434s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0108s
== 20200926073222 DeviseCreateUsers: migrated (0.0821s) =======================
```

######11ステータスを確認（やっとできた！！！！！）######
```terminal:terminal
〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200926073222  Devise create users
```

以上です。

#終わりに#

考察を実践してみましたが、
database.ymlの記載情報を変更するには、ロールバッグでは済まず、モデル作り直しになることがわかりました。

今回はモデルに大した内容を書いていなかったため、影響なしでしたが、
もしこれが、モデル含めたテーブル・カラム・レコード・マイグレーションなどの作り直しとなると手間がかかり大変ですよね。

utf8含めて、database.ymlの編集は、（一番初めに「rails db:create」を行う前に）しっかり要件定義をした上で、設定することが大事だと学びました。

あとは、今回「７６７byte」を超えない方法を考察しましたが、@terufumi1122さんの記事には「７６７byte」を超えてもいいようにするともありました。

エラーを解決するには、アプリケーションに目的に沿って、いろんな方面から柔軟に見る必要があることと、その引き出しを増やすために「WHY」の視点が大事だと改めて感じました。


見ていただいて、有り難うございました。
もし謎ファイルの原因わかる方や、誤っていることを載せていたら、ぜひ教えてください。