Rails6 のちょい足しな新機能を試す81（case_sensitive編）

# はじめに

Rails 6 に追加された新機能を試す第81段。 今回は、 `case_sensitive` 編です。
Rails 6 では、 MySQL を使っているときに、 `uniqueness` の validation を使っているときに警告が出るようになりました。

Ruby 2.6.4, Rails 6.0.0, MySQL 8.0.17 で確認しました。

```shell
$ rails --version
Rails 6.0.0
```

今回は、 User モデルを作成して `rails console` を使って確認します。

# プロジェクトを作る

```shell
rails new rails_sandbox
cd rails_sandbox
```

# User モデルを作る

name の属性をもつ User モデルを作ります。

```shell
bin/rails g model User name
```

# User モデルにバリデーションをつける

User の name 属性に uniqueness 属性をつけます。

```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, uniqueness: true
end
```

# seed データを登録する

User を1件だけ登録します。

```ruby:db/seed.rb
User.create(name: 'Taro')
```

# マイグレーションを実行し seed データを登録する

```shell
bin/rails db:create db:migrate db:seed
```

# rails console で確認する

`rails console` で確認します。

`User.new(name: 'taro')` で user オブジェクトを作ります。

```irb
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
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
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
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 にしたとき

```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: true }
end
```

これで validation のチェックをしてみましょう。

```irb
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 にしたとき

```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end
```

これで validation のチェックをしてみましょう。

```irb
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 が実行されていたらどうなるか、確認してみましょう。

```shell
$ bin/rails g migration user_name_column_to_binary
```

できたマイグレーションファイルを編集します。

```ruby:db/migrate/20190901051514_user_name_column_to_binary.rb
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 は以下のように戻しておきます。

```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, uniqueness: true
end
```

マイグレーションを実行し、seed データを登録し直します。

```shell
$ bin/rails db:migrate db:seed:replant
```

rails console で改めて確認します。

```irb
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
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
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

# 参考情報

* [What is new in Rails 6.0](https://bogdanvlviv.com/posts/ruby/rails/what-is-new-in-rails-6_0.html)
* [Deprecate mismatched collation comparison for uniquness validator](https://github.com/rails/rails/pull/35350)