tinyint(1)がtrue, falseなのはRailsの世界

今回はRailsネタ

検証環境は次の通りです

$ ruby -v 
ruby 2.5.0p0
$ bundle exec rails -v
Rails 5.2.0

例えば今回次のようなコマンドでstringのnameとbooleanのdisabledをもったUserモデルを作ったとしましょう

$ bundle exec rails g model User name:string disabled:boolean

migrationなどを済ませるとconsoleなどでcreateとかが出来るようになります

> User.create(name: "hoge", disabled: true)
#=> #<User id: 1, name: "hoge", disabled: true, created_at: "2018-05-05 05:51:58", updated_at: "2018-05-05 05:51:58">
> User.create(name: 'fuga', disabled: false)
# => #<User id: 2, name: "fuga", disabled: false, created_at: "2018-05-05 06:01:04", updated_at: "2018-05-05 06:01:04">
> user = User.find(1)
> user.name
#=> "hoge"
> user.disabled
#=> true

ここまでがRailsの世界となっていて一度dbを見てみます
今回はDBはmysql上で構築しています

先程作成したレコードが追加されているかを確認するためにSELECTしてみます

mysql> SELECT * FROM users;
+----+------+----------+---------------------+---------------------+
| id | name | disabled | created_at          | updated_at          |
+----+------+----------+---------------------+---------------------+
|  1 | hoge |        1 | 2018-05-05 06:00:56 | 2018-05-05 06:00:56 |
|  2 | fuga |        0 | 2018-05-05 06:01:04 | 2018-05-05 06:01:04 |
+----+------+----------+---------------------+---------------------+
2 rows in set (0.00 sec)

disabledはRails上でtrueだったものが1, falseだったものが0として格納されています
このdisabledはMySQL上のどの型で作成されているかでいうとtinyint(1)です

mysql> DESC users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| disabled   | tinyint(1)   | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

ここまではよく知った世界だと思います
ただこのtinyint(1)は0, 1以外も入って-128~127までいれることが出来ます
ここで一つ疑問として0, 1以外が入るとどうなるのでしょうか

Rails上で更新してみる

> User.find(1).update(disabled: 100)
> User.find(1).update(disabled: -100)

どちらもエラーは出ないですが、disabledはtrueになります
MySQL上でも1です

MySQL上で更新してみる

mysql> UPDATE users SET disabled = 100 where id = 1;
mysql> UPDATE users SET disabled = -100 where id = 1;

RailsUser.find(1).disabledしてみるとどちらもtrueがかえってきます

裏側どうなってるの?

RailsというよりはActiveRecordなのでActiveRecord側のコードを見てみます
※ ここからはコードをざっと読んで見てなので間違ってたらコメントください

まずtinyint(1)がRails上でbooleanとなるのはactive_record/connection_adapters/abstract_mysql_adapter.rbに定義されている
emulate_booleansがtrueの場合にtinyint(1)にType::Boolean.newが割り当てられる
でこのType::Booleanとは何ものかというとこちら このclass内のcast_valueに注目する

FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
~~~
def cast_value(value)
  if value == ""
    nil
  else
    !FALSE_VALUES.include?(value)
  end
end

valueとしてわたってきた空文字の場合はnil, それ以外で値がFALUSE_VALUESに該当するものがfalse, 該当しないものがtrueとなるのである
余談だけどFALSE_VALUESに該当すれば良いのでUser.create(name: "test", disabled: 'off')とすればfalseになりdb上は0が入る

動作的な部分はこれで理解は出来た!はず

Railsだけでシステムを構築していない場合

今回挙動を見たようにRails上でデータを扱っているとbooleanで1, 0が入ることはなさそう
ただRails以外もDBにアクセスしており、それはtinyint(1)に0, 1以外をいれている場合はどうするのか?

答えは先程コードを見た時のactive_record/connection_adapters/abstract_mysql_adapter.rbemulate_booleansでこれをfalseにしてあげれば良い
コード内にもコメンドで記載されている

config/application.rbに下記を追加する

require 'active_record/connection_adapters/mysql2_adapter'
ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false

ただこれ一つ問題があってアプリケーション全体にこの設定が適用されてしまうので、このカラムはbooleanで扱って、このカラムは数値で返すことが出来なくなる
この時はモデルに次のように指定してあげると個別カラムで数値で扱うことが出来る

class User < ApplicationRecord
  attribute :disabled, ActiveModel::Type::Integer.new
end

まとめ

今回はtinyint(1)のRails上での扱われ方を見ていった 今回のType moduleはstringとかdatetimeもあるので見てみるとどういった時にどのよにcastされるかわかるので面白い github.com