Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

ざっくり概要について

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

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

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

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

エラー内容について

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

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って何?

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

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

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

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

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

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

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

つまり、MySQLにおいて、

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

可能な文字数は、

  • 「utf8」は「3バイト」使うので、「767バイト➗3バイト=255文字まで」

  • 「utf8mb4」は「4バイト」使うので、「767バイト➗4バイト=191文字まで」

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

  • 「3バイト❌255文字=765バイト」(utf8)

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

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

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

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

冒頭で触れた通り、

まずは、上記方法で、解決しました!!!(ありがとうございます)

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

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

【それに対する考え方】
じゃあ、767byte超えないように、(「4バイト❌191文字=764バイト」)文字数の上限を191文字に設定しようよ!という方法です。(絵文字が使えるように、utf8bm4はそのまま活かす方法)

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

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
rails db:migrate

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

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



そこから、考察したこと

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

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

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

と思いました。

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

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

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

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

①いったんロールバック
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
〇〇@〇〇noMacBook-Air devise_app % rails db:migrate:status 

database: devise_app_development

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

③マイグレーションを実行(エラーを再現できたため、よし!)
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に書き直す
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
〇〇@〇〇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
〇〇@〇〇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
〇〇@〇〇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にならない
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してステータスを確認するも、000ファイルがdownにならない
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
〇〇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
〇〇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
〇〇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
〇〇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」の視点が大事だと改めて感じました。

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

cherry_2020
プログラマーを目指し、2020年8月から本格的に勉強しています!
https://cherry-2020.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away