2020年9月追記: Rails6.1 でこの問題に対応するようです。
このあたりに詳しく書かれています。
↓
Rails 6.0で"Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1."という警告が出たときの対処法 - Qiita
はじめに
Rails + MySQL でちょいちょいハマるので少しまとめてみました。
Railsの validation 機能は大変便利ですが、 uniqueness は presence や length よりも少し気を配る必要があります。
まず、「何を持ってユニーク(一意)とするか?」の意識をしなければなりません。
「大文字・小文字を同一視するか?」や「空白の扱いはどうするか?」などです。
ここでは大文字・小文字について考えてみます。尚、全角文字も考慮すると複雑になりすぎるのでここでは考えないことにします。空白問題も同様にここでは考えないことにします。
まず、知っておかなければならないのは 「デフォルトの大文字・小文字の扱いは Rails だと区別し MySQL は区別しない」 ということです。私はこの違いを忘れて何度かミスをしました。
ここでは Team モデルの name 属性について考えてみましょう。
以降、チーム名 Dragon
の1レコードが既に入っているとみなして考えてみます。
大文字・小文字を同一視する場合
例えばチーム名に Dragon
があれば DRAGON
や dragon
は許さない場合です。これは以下で対応できます。
以下で RDBMS(MySQL)側に UNIQUE 制約を付けています。
$ rails new uniqueness_test -d mysql
$ rails db:create
$ rails g scaffold Team name:uniq # ← UNIQUE キー制約付けてるよ
$ rails db:migrate
$ rails server
以下で case_sensitive: false
にすることで同一視しています。
class Team < ApplicationRecord
validates_uniqueness_of :name, case_sensitive: false # デフォルトは `true` だよ
end
ちなみにここで case_sensitive: true
にした場合、 DRAGON
を登録すると以下のようにエラーとなります。私はこの時、「あれ?ちゃんと validation 書いてるのに何で?チェックすり抜けた??」と一瞬混乱しちゃたりします。
大文字・小文字を同一視しない場合
例えばチーム名にて Dragon
と DRAGON
を別物とする場合です。
MySQL 側をいじらないといけないので面倒です。
先程の環境にマイグレーションを追加します。
ここでは MySQL ではカラムに対して BINARY 指定をします。1
class ChangeColumnNameOfTeam < ActiveRecord::Migration[5.1]
def up
execute("ALTER TABLE teams MODIFY name varchar(255) BINARY")
end
def down
execute("ALTER TABLE teams MODIFY name varchar(255)")
end
end
個人的にはここで MySQLに依存する形になってしまうので汎用性がなくなるのが嫌です。Rails側でもっとうまい書き方あるのかな?
あとはモデルを以下のようにしてDBと一致させます。
class Team < ApplicationRecord
validates_uniqueness_of :name, case_sensitive: true # case_sensitive はデフォルト true なので省略可
end
ちなみにフィールドが BINARY になっているかどうかは db/schema.rb を見ればわかります。(collation の部分が追加されます)
t.string "name", collation: "utf8_bin"
おわりに
validation の部分って Rails では気軽に書けるので便利ですよね。今回は DBに依存する部分が大きいと感じたので書いてみました。
一般的には例えば validates_length_of なんかも 厳密に定義して Rails のモデルとDB側を一致させるのかな?それだと変更に対応しづらいので DB側の文字列長はデフォルト。Railsのモデルで調整。などとしております。
あと、今回のテーマでは 空白の扱い についても注意が必要だと感じています。
以下のデータはチーム名として同一でしょうか?それとも異なりますか?登録を許しますか?
データ1: "Cat's EYE" ← 半角スペースが間に1つ
データ2: " Cat's EYE " ← 半角スペースが先頭と後方と単語中に1つずつ
データ3: " Cat's EYE " ← 半角スペースが沢山混ざってる
これについては別途、私なりの考えを書いてみようかな(気が向いたら...)
-
他にも COLLATE を使うなどの方法があります。詳しくは MySQL のドキュメントを参照して下さい。 ↩