Ruby on Rails + MySQL8 で条件によって structure.sql に差分が出てしまう話
この記事は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