🛤️

Ruby on Rails + MySQL8 で条件によって structure.sql に差分が出てしまう話

2024/12/10に公開

この記事はUMITRON Advent Calendar 2024 10日目の記事です。

起きている問題

弊社ではRuby on Rails と MySQL 8 を用いたプロジェクトがあります。このプロジェクトでDBのスキーマデータをstructure.sqlで管理しています。

この構成で開発をしていて、db:migrateを行った時に開発者の間でstructure.sqlに差分が出る問題が起きていました。

具体的には以下のようにvarchar(あるいはtext)のカラムにCHARACTER SETがついたりつかなかったりしていました。デフォルトがutf8mb4なので実質同じではありますが、開発する上で邪魔でした。

-  `message` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
+  `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,

この問題について調査しました。

原因調査

まずstructure.sql のCREATE TABLE文はmysqldumpコマンドがから生成されているようです。

次にMySQL側でCHARACTER SETがつくかどうかがどのように変わるのかをMySQLのソースコードを読んで調べました。

関連する箇所は以下です。

    if (field->has_charset()) {
      /*
        For string types dump charset name only if field charset is same as
        table charset or was explicitly assigned.
      */
      if (field->charset() != share->table_charset ||
          column_has_explicit_collation) {
        packet->append(STRING_WITH_LEN(" CHARACTER SET "));
        packet->append(field->charset()->csname);
      }

CHARACTER SETがつくかどうかの条件は field->charset() != share->table_charset がtrue、あるいは column_has_explicit_collation がtrueになることでした。カラムのcharacter setとテーブルのcharacter setは全部統一しているので field->charset() != share->table_charset は常にfalseになるはずなので、環境によって変わってしまうのは column_has_explicit_collation の値のようです。

名前とコメントからして、カラムを作るときにcollationを明示的に指定したかどうかを表すようです。そこでMySQL側でgeneral logを有効にして新規のマイグレーションファイルを作成してdb:migrateしてみたところ、CREATE TABLEのカラムの記述にはCHARACTER SETもCOLLATIONもついていませんでした。つまり、普通にdb:migrateしたテーブルは column_has_explicit_collation がfalseになるはずです。

ですが、先に示した実際に出る差分を見ると分かるとおり、structure.sqlには必ず COLLATION がついています。そこで、MySQLのCOLLATIONを表示するかどうかのコードを見てみます。

      /*
        For string types dump collation name only if
        collation is not primary for the given charset
        or was explicitly assigned.
      */
      if (!(field->charset()->state & MY_CS_PRIMARY) ||
          column_has_explicit_collation ||
          (field->charset() == &my_charset_utf8mb4_0900_ai_ci &&
           share->table_charset != &my_charset_utf8mb4_0900_ai_ci)) {
        packet->append(STRING_WITH_LEN(" COLLATE "));
        packet->append(field->charset()->m_coll_name);
      }

COLLATIONが表示される条件は以下ののどれかがtrueになることです。

  • !(field->charset()->state & MY_CS_PRIMARY)
  • column_has_explicit_collation
  • (field->charset() == &my_charset_utf8mb4_0900_ai_ci && share->table_charset != &my_charset_utf8mb4_0900_ai_ci)

3つ目の条件は、明確に意図を把握できてませんが、我々のケースではcollationは utf8mb4_general_ci を用いていますし、カラムとテーブルの character set と collation 統一していますので、trueになることはなさそうです。column_has_explicit_collation は前述の通り、通常falseになるはずです。となると !(field->charset()->state & MY_CS_PRIMARY) がtrueになるはずです。

調べたところ、MySQLにはcharacter setに対してデフォルトのcollationが存在し、 field->charset()->state & MY_CS_PRIMARY はcollationがそのデフォルトかどうかを示すようです。今character setは utf8mb4 で、これに対するデフォルトのcollationは utf8mb4_0900_ai_ci です。今使っているcollationは utf8mb4_general_ci なのでデフォルトではありません。なので、 !(field->charset()->state & MY_CS_PRIMARY) はtrueになります。ここが原因で、実行されるcreate table文にはCOLLATIONが明示的に指定されないが、structure.sqlにはCOLLATIONが指定されるようです。

また前述の通り、テーブルを作るmigrationを実行したときにはcollationの指定がされないのですが、このプロジェクトの開発を始める時のDBの初期化にするのに db:setup を実行すると、中で db:create, db:schema:load, db:seed が実行され、この db:schema:load でstructure.sql、つまりcollationが指定されたSQLが実行されます。

つまり、該当プロジェクトの開発を最初からしていてdb:migrateをし続けていると実行されるSQLにCOLLATEが明示的に指定されないのでstructure.sqlにCHARACTER SETがつかないが、db:setupを実行すると実行されるCOLLATEが明示的に指定されるためstructure.sqlにCHARACTER SETがつき、そこに差分が出るということが起きていました。

あとがき

開発中にstructure.sqlに差分がでてしまう問題を調査しました。まだ解決策は確定してないのですが、structure.sqlを実行すると問題が起きるので、dbのセットアップをするのにstructure.sqlが使われるdb:setupを使わずに、 db:create, db:migrate, db:seed を実行することを検討しています。


ウミトロンは、「持続可能な水産養殖を地球に実装する」というミッション実現に向けて、日々プロダクト開発・展開にチーム一丸となって邁進しています。
ウミトロンのニュースや活動状況を各種SNSで配信していますので、ぜひチェックいただき、来年も応援よろしくお願いします!
Facebook https://www.facebook.com/umitronaqtech/
X https://x.com/umitron
Instagram https://www.instagram.com/umitron.aqtech/
Linkedin https://www.linkedin.com/company/umitron

Discussion