Rails の validates_uniqueness_of と MySQL の UNIQUEキー制約について

はじめに

Rails + MySQL でちょいちょいハマるので少しまとめてみました。

Railsの validation 機能は大変便利ですが、 uniqueness は presence や length よりも少し気を配る必要があります。

まず、「何を持ってユニーク(一意)とするか?」の意識をしなければなりません。
「大文字・小文字を同一視するか?」や「空白の扱いはどうするか?」などです。

ここでは大文字・小文字について考えてみます。尚、全角文字も考慮すると複雑になりすぎるのでここでは考えないことにします。空白問題も同様にここでは考えないことにします。

まず、知っておかなければならないのは 「デフォルトの大文字・小文字の扱いは Rails だと区別し MySQL は区別しない」 ということです。私はこの違いを忘れて何度かミスをしました。

ここでは Team モデルの name 属性について考えてみましょう。
以降、チーム名 Dragon の1レコードが既に入っているとみなして考えてみます。

大文字・小文字を同一視する場合

validation error

例えばチーム名に Dragonがあれば DRAGONdragon は許さない場合です。これは以下で対応できます。

以下で 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 にすることで同一視しています。

app/models/team.rb
class Team < ApplicationRecord
  validates_uniqueness_of :name, case_sensitive: false # デフォルトは `true` だよ
end

ちなみにここで case_sensitive: true にした場合、 DRAGON を登録すると以下のようにエラーとなります。私はこの時、「あれ?ちゃんと validation 書いてるのに何で?チェックすり抜けた??」と一瞬混乱しちゃたりします。

1515725671844.jpg

大文字・小文字を同一視しない場合

例えばチーム名にて DragonDRAGON を別物とする場合です。

UniquenessTest - http___localhost_3000_teams.png

MySQL 側をいじらないといけないので面倒です。
先程の環境にマイグレーションを追加します。

ここでは MySQL ではカラムに対して BINARY 指定をします。1

db/migrate/20180108034452_change_column_name_of_team.rb
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と一致させます。

app/models/team.rb
class Team < ApplicationRecord
  validates_uniqueness_of :name, case_sensitive: true # case_sensitive はデフォルト true なので省略可
end

ちなみにフィールドが BINARY になっているかどうかは db/schema.rb を見ればわかります。(collation の部分が追加されます)

db/schema.rbの抜粋
     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 " ← 半角スペースが沢山混ざってる

これについては別途、私なりの考えを書いてみようかな(気が向いたら...)



  1. 他にも COLLATE を使うなどの方法があります。詳しくは MySQL のドキュメントを参照して下さい。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.