🪤

MySQL8とRailsの組み合わせでcollationの罠にハマった話

2024/07/06に公開
1

こんにちは、Yuanです!Railsのアプリケーションをいじってます。
今日は、RailsアプリケーションのデータベースをMySQL5.7からMySQL8.0へメジャーアップデートしたときに、collation(照合順序)の設定で罠にハマったため、その対策と原因についてまとめます。

まとめから言うと?

下記の今北産業(三行要約)です。サクッと解決策を知りたい方はこちらで十分だと思います。

  1. Railsのdatabase.ymlcharsetcollationの設定をちゃんと書こうね
  2. MySQL5.7とMySQL8.0は文字コードutf8mb4に対応するデフォルトのcollationが違うよ(utf8mb4_general_ciutf8mb4_0900_ai_ci
  3. mysqldの設定にcharacter-set-servercollation-serverを入れてもrails db:createするときに意味がないよ

そもそも

今回、MySQLのアップデート対象のRailsアプリケーションは、database.ymlcharsetcollationも設定されていませんでした。
そのため、このアプリケーションのデータベースは、MySQL5.7のデフォルト文字コードであるutf8mb4に対応した照合順序であるutf8mb4_general_ciが自動的に使われて続けている状態です。

しかし、MySQLを5.7から8.0にメジャーアップデートする必要にあたって、一つの問題点があります。
それは、MySQL8.0では文字コードutf8mb4にデフォルトで対応する照合順序がutf8mb4_0900_ai_ciに変わったと言うことです。
この照合順序が変わると、文字を使ったソートの順番が変わったり、異なる文字を同じ文字とみなす(🍣=🍺問題)ルールが変わります[1]
そのため、このアプリケーションのMySQLのアップデートでは照合順序を維持する必要がありました。

何に困った?

ローカルの開発環境をMySQL8.0で新たに作ったときに問題が発生しました。
それは、本番環境とローカル環境で照合順序(collation)が異なってしまう問題です。

MySQLのメジャーアップデートは、すでにあるデータベースに対しては設定済みの照合順序を維持してくれるため、アップデートに関して特に気にする必要はありません。
しかし、開発環境などで新規にデータベースを作ったときに、MySQLのデフォルトの照合順序が本番環境ズレてしまいます。
このズレは、「本番環境だけで発生するバグ」の原因となることがあるため、防がなければなりません。

どうやったら対処できる?

とても簡単です。Railsアプリケーションのdatabase.ymlcollation: utf8mb4_general_ciを追加すれば良いだけです。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  charset: utf8mb4
  collation: utf8mb4_general_ci

これにより、MySQL8.0で新規にデーターベースを作るときも、照合順序をutf8mb4_general_ciに固定することができます。

Q&A

mysqldの起動のパラメータやmy.cnfで照合順序を固定する方法ではダメなの?

先に結論から言うとダメです。
database.ymlcollationを指定してください。

この質問は、mysqldの起動パラメータやmy.cnfに下記を設定することを指していますが、後述のRailsの実装による理由のためrails db:createでデータベースを作るときに反映されません。

mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

なぜ、これらが意味がないかというとrails db:createのコードを読むと下記のように書かれているためです。

      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

https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L283-L293

このコードのとおり、app_developmentデータベースに対してcharsetcollationが設定されていない場合は、CREATE DATABASE app_development DEFAULT CHARACTER SET utf8mb4が実行されます。
MySQLは、データーベースを作るときに文字コードのみが指定されている場合は、その文字コードに対応したMySQL内部のデフォルトの照合順序を使います[2]
すなわち、起動のパラメータやmy.cnfで設定されたものを無視して、MySQLに内蔵されたマッピングテーブルを元に決定されます(このテーブルはshow collationで確認できます)。

以上から、rails db:createでデータベースを作るときは、mysqldの起動のパラメータやmy.cnfで照合順序を固定する方法は使えません。
他にも、MySQLのアップデートなどでcollationが変わることを良しとしない場合は、database.ymlにcollationを書いておいた方が良いことがわかります。

まとめ

最初にも書いているとおりに、下記を守れば開発環境と本番環境で照合順序の違いが発生する可能性を減らすことができます。

  1. Railsのdatabase.ymlcharsetcollationの設定をちゃんと書こうね
  2. MySQL5.7とMySQL8.0は文字コードutf8mb4に対応するデフォルトのcollationが違うよ(utf8mb4_general_ciutf8mb4_0900_ai_ci
  3. mysqldの設定にcharacter-set-servercollation-serverを入れてもrails db:createするときに意味がないよ

最後に

この問題に躓いてから、原因を特定できるまで約一日を使ってしまいました😭。
ただ、文字コードと照合順序に関して詳しくなれたのでよしとします!

もし、MySQL8アップデートを計画されている方にこの記事が役立つことがあれば幸いです。

質問がありましたら、コメント欄にお気軽にどうぞ!

脚注
  1. https://qiita.com/woody1033kt/items/612b7d951ce036a39fb6 ↩︎

  2. https://dev.mysql.com/doc/refman/8.0/ja/charset-database.html ↩︎

Discussion

YuanYuan

encodingとcharsetは別物だとわかったため、記事を修正しました。