[Rails]mySQL8.0のデフォルトのcollationとActiveRecordの差分に困った話

2023/07/19

Rails
mySQL

こんにちは。

mySQL8.0が出てからしばらく経ちますが、まだまだ8.0に移行していないプロジェクトも多いのではないでしょうか。

私が担当している案件でも、mySQL8.0に移行することになりました。

その際に、mySQL8.0のデフォルトのcollationとActiveRecordの差分にハマったので、その概要をまとめてみます。

ActiveRecordとは

こちらの説明がわかりやすかったので引用します。

Active Recordとは、MVCで言うところのM、つまりモデルに相当するものであり、ビジネスデータとビジネスロジックを表すシステムの階層です。Active Recordは、データベースに恒久的に保存される必要のあるビジネスオブジェクトの作成と利用を円滑に行なえるようにします。Active Recordは、ORM(オブジェクト/リレーショナルマッピング)システムに記述されている「Active Recordパターン」を実装したものであり、このパターンと同じ名前が付けられています。

引用: Active Recordとは

collationとは

まずは、collationとは何かを説明します。

collationとは、データベースのテーブルのカラムの文字列の比較やソートのルールを定義するものです。

例えば、日本語の場合は、utf8mb4_ja_0900_as_csというcollationがあります。

このcollationを指定すると、は同じ文字として扱われます。

こちらの記事に詳しく書かれていますので、興味がある方は読んでみてください。

mySQL8.0のデフォルトのcollation

mySQL8.0のデフォルトのcollationは、utf8mb4_0900_ai_ciです。

(2023/07/19現在の最新のMySQLのバージョンは、8.0.34です。)

このcollationを指定すると、は異なる文字として扱われます。

しかし、例えば榊󠄀は同じ文字として扱われます。

ActiveRecordではどのようになっているか

ActiveRecordでは、文字は文字コードで単純に扱われます。

そのため、は異なる文字として扱われるのはもちろん、榊󠄀も異なる文字として扱われます。

どんなことが起こるか

なので、例えば以下のようなケースで、データの保存に失敗することがあります。


# モデル
class User < ApplicationRecord
  validates :name, uniqueness: true
end

# データ

# name: '榊'
User.create(name: '榊')
'榊'.codepoints.map {|i| i.to_s(16) }
=> ["698a"]

# name: '榊󠄀'
User.create(name: '榊󠄀')
'榊󠄀'.codepoints.map {|i| i.to_s(16) }
=> ["698a", "e0100"]

この場合、榊󠄀は異なる文字として扱われるので、nameカラムのuniqunessバリデーションには引っかかりません。

しかし、mySQL8.0のデフォルトのcollationでは、榊󠄀は同じ文字として扱われるので、nameカラムにunique制約がある場合、データの保存に失敗します。

対応方法

対応方法は、以下の3つが考えられます。

  1. mySQL8.0のデフォルトのcollationを変更する
  2. mySQL側のユニーク制約を外す、見直す
  3. 異体字を正規化して扱う

今回は、2の対応方法を採用しました。

1を採用しなかった理由としては、すでに多数のレコードとテーブルが存在しており、それらをすべて変更するのは現実できじゃないと判断したためです。

3については、今回は採用しませんでしたが、異体字を正規化して扱うことで、異体字を正規化した文字に統一することができます。

しかし、異体字を正規化するとそもそもわざわざ異体字を使う意味がなくなってしまうので、採用するかどうかは検討が必要です。

itaijiというgemがあるので、それを使うことも検討してみてもいいかもしれません。