はじめに
Rails 6 に追加された新機能を試す第81段。 今回は、 case_sensitive
編です。
Rails 6 では、 MySQL を使っているときに、 uniqueness
の validation を使っているときに警告が出るようになりました。
Ruby 2.6.4, Rails 6.0.0, MySQL 8.0.17 で確認しました。
$ rails --version
Rails 6.0.0
今回は、 User モデルを作成して rails console
を使って確認します。
プロジェクトを作る
rails new rails_sandbox
cd rails_sandbox
User モデルを作る
name の属性をもつ User モデルを作ります。
bin/rails g model User name
User モデルにバリデーションをつける
User の name 属性に uniqueness 属性をつけます。
class User < ApplicationRecord
validates :name, uniqueness: true
end
seed データを登録する
User を1件だけ登録します。
User.create(name: 'Taro')
マイグレーションを実行し seed データを登録する
bin/rails db:create db:migrate db:seed
rails console で確認する
rails console
で確認します。
User.new(name: 'taro')
で user オブジェクトを作ります。
irb(main):001:0> user = User.new(name: 'taro')
(0.4ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, name: "taro", created_at: nil, updated_at: nil>
user.valid?
で validation を実行します。
irb(main):002:0> user.valid?
DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :name attribute in User model, pass `case_sensitive: true` option explicitly to the uniqueness validator. (called from irb_binding at (irb):2)
User Exists? (0.9ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = BINARY 'taro' LIMIT 1
=> true
DEPRECATION WARNING が表示されることがわかります。
また、 validation のチェックをする SQL の WHERE 句 が
WHERE `users`.`name` = BINARY 'taro'
と BINARY
がついていることに注意してください。
この動作の問題点
user を保存します。
irb(main):003:0> user.save
DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :name attribute in User model, pass `case_sensitive: true` option explicitly to the uniqueness validator. (called from irb_binding at (irb):3)
(0.5ms) BEGIN
User Exists? (0.7ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = BINARY 'taro' LIMIT 1
User Create (0.7ms) INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('taro', '2019-09-01 04:37:55.708725', '2019-09-01 04:37:55.708725')
(13.0ms) COMMIT
=> true
name が taro
であるデータを検索します。
irb(main):006:0> User.where(name: 'taro').count
(0.8ms) SELECT COUNT(*) FROM `users` WHERE `users`.`name` = 'taro'
=> 2
2件見つかりました。name が Taro
と taro
の両方のデータが検索されてしまうのです。
という訳で
Rails 6.1 以降では、この不統一な動作をやめて、デフォルトでは、大文字小文字を区別しない validation になるのではないかと思われます。
これまで通りの動作をさせたい場合には、 :case_sensitive オプションを使います。
:case_sensitive を true にしたとき
class User < ApplicationRecord
validates :name, uniqueness: { case_sensitive: true }
end
これで validation のチェックをしてみましょう。
irb(main):006:0> reload!
Reloading...
=> true
irb(main):007:0> user = User.new(name: 'taRo')
=> #<User id: nil, name: "taRo", created_at: nil, updated_at: nil>
irb(main):008:0> user.valid?
User Exists? (0.8ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = BINARY 'taRo' LIMIT 1
=> true
こんどは、ワーニングが出ないようになりました。
:case_sensitive を false にしたとき
class User < ApplicationRecord
validates :name, uniqueness: { case_sensitive: false }
end
これで validation のチェックをしてみましょう。
irb(main):009:0> reload!
Reloading...
=> true
irb(main):010:0> user = User.new(name: 'taRo')
=> #<User id: nil, name: "taRo", created_at: nil, updated_at: nil>
irb(main):011:0> user.valid?
User Exists? (0.8ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = 'taRo' LIMIT 1
=> false
今度は、警告は出ないですが、 validation も大文字小文字の区別をしないでチェックしていることに注意してください。
user.valid?
の結果が false
ですし、validation で使われる SQL の where 句も BINARY がついてません。
そもそも MySQL で大文字小文字の区別をするようにしていたらどうなるか?
では、先に MySQL 側で大文字小文字を区別するように、migration が実行されていたらどうなるか、確認してみましょう。
$ bin/rails g migration user_name_column_to_binary
できたマイグレーションファイルを編集します。
class UserNameColumnToBinary < ActiveRecord::Migration[6.0]
def up
execute('ALTER TABLE users MODIFY name varchar(255) BINARY')
end
def down
execute('ALTER TABLE users MODIFY name varchar(255)')
end
end
model は以下のように戻しておきます。
class User < ApplicationRecord
validates :name, uniqueness: true
end
マイグレーションを実行し、seed データを登録し直します。
$ bin/rails db:migrate db:seed:replant
rails console で改めて確認します。
irb(main):001:0> user = User.new(name: 'taro')
(0.3ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, name: "taro", created_at: nil, updated_at: nil>
valid?
メソッドを実行しても警告はでません。
irb(main):002:0> user.valid?
User Exists? (0.4ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = 'taro' LIMIT 1
=> true
user を保存して、 taro
を検索すると 1件です。
irb(main):003:0> user.save
(0.5ms) BEGIN
User Exists? (0.7ms) SELECT 1 AS one FROM `users` WHERE `users`.`name` = 'taro' LIMIT 1
User Create (0.3ms) INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('taro', '2019-09-01 05:32:29.403443', '2019-09-01 05:32:29.403443')
(13.0ms) COMMIT
=> true
irb(main):004:0> User.where(name: 'taro').count
(0.7ms) SELECT COUNT(*) FROM `users` WHERE `users`.`name` = 'taro'
=> 1
この場合は、警告もなく、矛盾なく動作しているようです。
蛇足ですが、 カラム単位でなくテーブル単位で大文字小文字を区別する設定もできるようです(が未確認です)。
また、 MySQL の DB 自体 ( config/database.yml
) でまとめて大文字小文字を区別する設定する方法もありそうな気がするのですが、見つけられませんでした。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try081_mysql_comparison
MySQL で大文字小文字の区別をするようにしていた場合
https://github.com/suketa/rails_sandbox/tree/try081_mysql_comparison_user_name_column_to_binary