#ざっくり概要について#
このエラーは「Mysql2に、格納できる文字データは、767バイトまでだよ!今のままでは、Mysqlにマイグレーションできないよ!」という意味で理解しました。
そのエラーを踏まえて、この記事では、以下2つの方法を実践しました。
-
database.ymlが「utf8mb4」のまま、「文字制限をかけるファイルを新規作成」する方法
(@terufumi1122さんの記事のおかげで解決できました) - 「文字制限をかける新規ファイル」を作成せずに、database.ymlを「utf8」に書き直す方法(この方法はやらないほうがいいと学びました)
その上で一番大事だなと思ったことは、 MySQLを使用する際、
「絵文字を使えるかどうか、要件定義の際にしっかり決める」ということです。
では、詳細にいきましょう!!!
#エラー内容について#
「Mysql2::Error: Specified key was too long; max key length is 767 bytes」というエラーが発生。
〇〇@〇〇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って何?#
その概念が分からず、自分なりにですが、以下のように理解しました。
-
文字コードとは、「あ」だったら1番、「い」だったら2番というように、文字に対して割り振っているコードのこと。(実際には進数など使って、もっと複雑そうですが・・・)
-
「utf8」とは文字コードの中でも、世界で最も普及している文字コード。「utf8」は1~4バイトで文字を表現するが、MySQLでは3バイトの文字までしか扱えない。
-
「utf8mb4」とは、データベースMySQLで扱うための文字コード。Mysqlにおいて、絵文字などは「utf8」の4バイトに当たるので、「utf8mb4」でないと絵文字が扱えない。
-
MySQLにおいて、保存できる文字数は「255文字」まで。
####こちらから引用させていただきました###
- 「utf8」と「utf8mb4」の違いについて
- 文字コードとは
- 単一カラムインデックスの最大キー長は767バイトまで
- MysqlにおけるVARCHAR型とは
- Railsにおけるstringのデフォルト値255文字まで
#上記を組み合わせて、理解したこと#
つまり、MySQLにおいて、
- utf8は文字を扱う文字コード、utf8bm4は絵文字を扱う文字コード
可能な文字数は、
-
「utf8」は「3バイト」使うので、「767バイト➗3バイト=255文字まで」
-
「utf8mb4」は「4バイト」使うので、「767バイト➗4バイト=191文字まで」
stringにおけるデフォルト値が255文字なので、
-
「3バイト❌255文字=765バイト」(utf8)
-
「4バイト❌255文字=1020バイト」(utf8bm4)MySQLで保存できる767byte超えてるよ!!今回はこの状態に陥っており、エラーになっている
ということが、わかりました。
#その上で、エラー解決方法へ#
さて、エラー内容が起きた原因がすっきりしたところで、肝心の解決方法についてです。
冒頭で触れた通り、
まずは、上記方法で、解決しました!!!(ありがとうございます)
解決方法について、概要を話すと、
【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4)MySQLで保存できる767byte超えてるよ!!今回はこの状態に陥っており、エラーになっている
【それに対する考え方】
じゃあ、767byte超えないように、(「4バイト❌191文字=764バイト」)文字数の上限を191文字に設定しようよ!という方法です。(絵文字が使えるように、utf8bm4はそのまま活かす方法)
以下のようなmysql.rbをconfig/initializer配下に新規作成します。(以下のコード含め、@terufumi1122さんの記事から引用しています。)
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
上記ファイルを入力した後に、
rails db:migrate
をしたところ、無事にマイグレートされました!!!
(MySQLであるSequelProを見たところ、カラムができていました。)
【2021/1/12追記】
もし、マイグレートした際StandardError: An error has occurred, all later migrations canceled:
とエラーが出たら・・・
-
rails db:migrate:status
でdown
になっているか確認 -
rails db:reset
の実行 -
rails db:migrate
の再実行で上手くいった
#そこから、考察したこと#
さて、無事に解決したのですが、問題点に対して、767byteを超えない方法について、下記のような考え方もできないかなと思いました。
【問題点】
「4バイト❌255文字=1020バイト」(utf8bm4)MySQLで保存できる767byte超えてるよ!!今回はこの状態に陥っており、エラーになっている
【それに対する考え方】
じゃあ、もし、そのアプリケーションで絵文字を使わないのであれば、「3バイト❌255文字=765バイト」(utf8)に変更するのはどうか。つまり、191文字以下にするファイルを作成するのではなく、「config>database.yml」に記載している、文字コードを、「utf8bm4」から「utf8」に書き換えて、マイグレーションすればいいのでは!?
と思いました。
ここから実践する訳ですが、結論から言うと、「モデルから作り直しになる(マイグレーションの書き換えでは済まない)」ということを学びました。(つまり、面倒で大変です。)
「マイグレーションの書き換えで済まない」とは、「rails db:migrate」と共に、(指示をしていない)謎のマイグレーションファイルが出現し、そのマイグレーションファイルは不要なので、架空ファイルに変更して、ドロップやリセットを行いますが消そうとしても消えません。
「rails d devise user」でいったんモデルを消して、「rails g devise user」で作り直してやっと、エラーの出ない「utf8」文字コードのアプリケーションができました。
####いざ、実践!(先ほどのエラー解決した状況から、上記考察を実践しています)#####
######①いったんロールバック######
〇〇@〇〇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になっているので、よし!)######
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status
database: devise_app_development
Status Migration ID Migration Name
--------------------------------------------------
down 20200925212154 Devise create users
######③マイグレーションを実行(エラーを再現できたため、よし!)######
〇〇@〇〇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に書き直す######
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
socket: /tmp/mysql.sock
######⑤rails db:resetを実行(良い感じ!)######
〇〇@〇〇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を実行(良い感じ!)######
〇〇@〇〇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のファイルってなんだ?)######
〇〇@〇〇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)にマイグレーションを修正して、ロールバックするも、000ファイルがdownにならない######
〇〇@〇〇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してステータスを確認するも、000ファイルがdownにならない######
〇〇@〇〇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
######⑧モデルを削除する######
〇〇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
######⑨モデルを作り直す######
〇〇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
######⑩マイグレーションを行う######
〇〇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ステータスを確認(やっとできた!!!!!)######
〇〇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」を行う前に)しっかり要件定義をした上で、設定することが大事だと学びました。
あとは、今回「767byte」を超えない方法を考察しましたが、@terufumi1122さんの記事には「767byte」を超えてもいいようにするともありました。
エラーを解決するには、アプリケーションに目的に沿って、いろんな方面から柔軟に見る必要があることと、その引き出しを増やすために「WHY」の視点が大事だと改めて感じました。
見ていただいて、有り難うございました。
もし謎ファイルの原因わかる方や、誤っていることを載せていたら、ぜひ教えてください。