tl;dr (最初に結論)
Railsのモデルに新しくboolean型のカラムを追加するときは必ずデフォルト値を設定しておいた方がいいです。
すなわち、
# NG
add_column :users, :notification_allowed, :boolean
ではなく、
# OK
add_column :users, :notification_allowed, :boolean, default: false, null: false
のように書きましょう、ということです。
default: false
はデフォルト値を false
に設定することを示しています。
なので既存のレコードは notification_allowed
カラムの値が false
になります。
null: false
は NULL
(Rubyでいうところの nil
)が設定されることを禁止するオプションです。
必須ではありませんが、NULL
が入ると面倒な問題を引き起こしやすいので基本的に付けておくことをオススメします。
それではこの件に関する詳しい内容を以下で説明していきます。
データベースとRubyで異なる NULL / nil の扱い
デフォルト値を明示的に設定しなかった場合、新しいカラムには NULL
が入ります。
データベース(SQL)の世界において、NULL
は特殊な存在です。
NULL
は false
の意味でもなく、「空(から)」の意味でもありません。
NULL
は 「まだ決まっていないから何も評価できない」 の意味になります。(NULL
の意味づけは文献によって多少異なるので、適宜ネットや技術書も参考にしてください)
つまり、NULL
は 「true
でも false
でもない状態」 を表します。
よって、Boolean型のカラムに NULL
が入ると、「true
/ false
/ NULL
」の3つの状態が存在することになるのです。
一方、Rubyでは 「nil
と false
はどちらも偽」 として扱われます。
このようにデータベースとRubyでは NULL
/ nil
の取り扱いルールが異なっています。
なので、データの検索の考え方が非常にややこしくなります。
実際のRailsアプリケーションで確認してみる
言葉の説明だけではピンと来ない人もいると思うので、具体的なデータを使って確認してみましょう。
以下のサンプルコードの実行環境は以下の通りです。
- Rails 4.2.1
- PostgreSQL 9.4.3
ここではPostgreSQLを使っていますが、MySQLでも同じ結果になります。
さて、このアプリケーションには User クラスがあります。
データベースには3件のレコードが入っています。
id | name | created_at | updated_at
----+---------+----------------------------+-----------------------------
1 | Alice | 2015-06-09 19:31:19.874835 | 2015-06-09 19:31:19.874835
2 | Bob | 2015-06-09 19:31:26.632052 | 2015-06-09 19:31:26.632052
3 | Charlie | 2015-06-09 19:31:41.060407 | 2015-06-09 19:31:41.060407
このクラスに「メール通知を受け取るフラグ(notification_allowed
)」を追加します。
migrationファイルには特にデフォルト値を設定しません。
class AddNotificationAllowedToUsers < ActiveRecord::Migration
def change
add_column :users, :notification_allowed, :boolean
end
end
マイグレーションを実行すると、マイグレーション直後のデータは次のようになります。
id | name | created_at | updated_at | notification_allowed
----+---------+----------------------------+----------------------------+----------------------
1 | Alice | 2015-06-09 19:31:19.874835 | 2015-06-09 19:31:19.874835 |
2 | Bob | 2015-06-09 19:31:26.632052 | 2015-06-09 19:31:26.632052 |
3 | Charlie | 2015-06-09 19:31:41.060407 | 2015-06-09 19:31:41.060407 |
ご覧の通り、notification_allowed
は空欄になっています。これは NULL
が入っている状態です。
さて、画面からこのデータを更新してみます。
Aliceさんは「メール通知を受け取る」設定にします。
Charlie さんは「メール通知を受け取る」を変更せずに、自分の名前だけを変更しました。(それと同時に notification_allowed
も false
で更新されます)
その結果、データはこのようになりました。
id | name | created_at | updated_at | notification_allowed
----+-------+----------------------------+----------------------------+----------------------
1 | Alice | 2015-06-09 19:31:19.874835 | 2015-06-09 19:38:59.290501 | t
2 | Bob | 2015-06-09 19:31:26.632052 | 2015-06-09 19:31:26.632052 |
3 | Chris | 2015-06-09 19:31:41.060407 | 2015-06-09 19:39:29.294103 | f
ご覧の通り、notification_allowed
には t
/ NULL
/ f
の3種類が存在しています。
それでは rails console
で、「通知を受け取るユーザ」を検索してみましょう。
irb(main):004:0> User.where(notification_allowed: true)
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."notification_allowed" = 't'
=> #<ActiveRecord::Relation [#<User id: 1, name: "Alice", created_at: "2015-06-09 19:31:19", updated_at: "2015-06-09 19:38:59", notification_allowed: true>]>
Aliceさんが返ってきました。
続いて、「通知を受け取らないユーザ」を検索してみます。
irb(main):005:0> User.where(notification_allowed: false)
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."notification_allowed" = 'f'
=> #<ActiveRecord::Relation [#<User id: 3, name: "Chris", created_at: "2015-06-09 19:31:41", updated_at: "2015-06-09 19:39:29", notification_allowed: false>]>
ここで返ってくるのはChrisさんだけです。Bobさんは含まれません。
Bobさんを検索するには次のように「通知を受け取るフラグが NULL
のユーザ」を検索しなければいけません。
irb(main):006:0> User.where(notification_allowed: nil)
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."notification_allowed" IS NULL
=> #<ActiveRecord::Relation [#<User id: 2, name: "Bob", created_at: "2015-06-09 19:31:26", updated_at: "2015-06-09 19:31:26", notification_allowed: nil>]>
しかし、現実の世界ではこの3つの状態をわざわざ使い分けたいと思うケースは滅多にないでしょう。
ふつうは「通知を受け取る」「受け取らない」のどちらかになっていることを期待するはずです。
デフォルト値の設定が必要になるのはこのためです。
また、万一 NULL
が入るとこのややこしい問題が再浮上してしまうので、データベースレベルで NULL
が入ることを禁止しておく(NOT NULL制約を付ける)方がベターです。
つまり、migrationファイルはこのように書くべきでした。
class AddNotificationAllowedToUsers < ActiveRecord::Migration
def change
add_column :users, :notification_allowed, :boolean, default: false, null: false
end
end
こうしておけば、既存のレコードは最初から false
の状態になります。
id | name | created_at | updated_at | notification_allowed
----+---------+----------------------------+----------------------------+----------------------
1 | Alice | 2015-06-09 19:31:19.874835 | 2015-06-09 19:31:19.874835 | f
2 | Bob | 2015-06-09 19:31:26.632052 | 2015-06-09 19:31:26.632052 | f
3 | Charlie | 2015-06-09 19:31:41.060407 | 2015-06-09 19:31:41.060407 | f
NULL
は入ってこないので、検索する場合も true
と false
のどちらかを指定するだけで済みます。
# 「通知を受け取るユーザ」を検索
User.where(notification_allowed: true)
# 「通知を受け取らないユーザ」を検索
User.where(notification_allowed: false)
# NULLのデータは絶対に存在しないので実行結果は常に0件
User.where(notification_allowed: nil)
NULL は false と見なして検索する方法
「気づくのが遅かった!すでにデータに NULL
が入っちゃってます!」という場合は以下のようにすれば NULL
と false
のデータを同時に検索できます。
(NULL
は false
として扱う場合)
irb(main):001:0> User.where("notification_allowed = ? OR notification_allowed IS NULL", false)
User Load (0.5ms) SELECT "users".* FROM "users" WHERE (notification_allowed = 'f' OR notification_allowed IS NULL)
=> #<ActiveRecord::Relation [#<User id: 2, name: "Bob", created_at: "2015-06-09 19:31:26", updated_at: "2015-06-09 19:31:26", notification_allowed: nil, banned: false>, #<User id: 3, name: "Chris", created_at: "2015-06-09 19:31:41", updated_at: "2015-06-09 19:39:29", notification_allowed: false, banned: false>]>
既存の NULL のレコードを一気に false で更新する方法
既存のレコードに NULL
が含まれる場合、毎回 NULL
を考慮しながら検索するのは面倒です。
それよりも既存のレコードから一気に NULL
を消し去ってしまう方があとあとの効率が良さそうです。
以下のようにすればSQL一発で NULL
のレコードを全件 false
に変換できます。
irb(main):008:0> User.where(notification_allowed: nil).update_all(notification_allowed: false)
SQL (2.2ms) UPDATE "users" SET "notification_allowed" = 'f' WHERE "users"."notification_allowed" IS NULL
=> 1
あとから NOT NULL制約を追加する方法
既存のレコードから NULL
を消し去ったら、さらにマイグレーションを追加して NULL
を禁止しておくと安全性がより向上します。
以下はあとから NOT NULL制約を追加するmigrationファイルの記述例です。
class ChangeNotificationAllowedInUsers < ActiveRecord::Migration
def change
change_column :users, :notification_allowed, :boolean, null: false
end
end
ただし、このマイグレーションを実行する場合はあらかじめ既存のレコードから NULL
のレコードをなくしておく必要があります。
NULL
のレコードがあると下記のようなエラーが発生して NOT NULL
制約を付与することができません。
$ bin/rake db:migrate
== 20150609212340 ChangeNotificationAllowedInUsers: migrating =================
-- change_column(:users, :notification_allowed, :boolean, {:null=>false})
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::NotNullViolation: ERROR: column "notification_allowed" contains null values
: ALTER TABLE "users" ALTER "notification_allowed" SET NOT NULL/Users/jit/dev/sandbox/boolean-default-postgres/db/migrate/20150609212340_change_notification_allowed_in_users.rb:3:in `change'
-e:1:in `<main>'
ActiveRecord::StatementInvalid: PG::NotNullViolation: ERROR: column "notification_allowed" contains null values
: ALTER TABLE "users" ALTER "notification_allowed" SET NOT NULL
/Users/jit/dev/sandbox/boolean-default-postgres/db/migrate/20150609212340_change_notification_allowed_in_users.rb:3:in `change'
-e:1:in `<main>'
PG::NotNullViolation: ERROR: column "notification_allowed" contains null values
/Users/jit/dev/sandbox/boolean-default-postgres/db/migrate/20150609212340_change_notification_allowed_in_users.rb:3:in `change'
-e:1:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
MySQLで実行した場合
前述の通り、MySQLで実行してもPostgreSQLの場合と同じ結果になります。
+----+-------+---------------------+---------------------+----------------------+
| id | name | created_at | updated_at | notification_allowed |
+----+-------+---------------------+---------------------+----------------------+
| 1 | Alice | 2015-06-09 19:51:41 | 2015-06-09 19:56:28 | 1 |
| 2 | Bob | 2015-06-09 19:51:48 | 2015-06-09 20:31:23 | NULL |
| 3 | Chris | 2015-06-09 19:51:57 | 2015-06-09 19:56:37 | 0 |
+----+-------+---------------------+---------------------+----------------------+
上の図のように、PostgreSQLは t
/ f
、MySQLは 1
/ 0
になりますが、その違いはRailsが吸収してくれるのでコードを書くときは特に意識する必要はありません。
ちなみに、以下は僕が実際に試したMySQLでの実行環境です。
- Rails 4.2.1
- MySQL 5.6.25
疑わしいmigrationファイルが存在しないかgrep検索する方法
One more thing.
この記事を読んで「あー、そんなこと今まで気にしてなかったわ。もしかしたらやっちゃってるかも・・・」と心配になってきた人はRailsプロジェクトのディレクトリで以下のコマンドを実行してみてください。
grep -e 'add_column.*boolean' db/migrate/* | grep -v 'default'
これを実行するとデフォルト値なしでboolean型のカラムを追加したmigrationファイルを一覧表示してくれます。(以下はMac OS Xでの実行例です)
$ grep -e 'add_column.*boolean' db/migrate/* | grep -v 'default'
db/migrate/20150609193431_add_notification_allowed_to_users.rb: add_column :users, :notification_allowed, :boolean
該当するmigrationファイルが見つかった場合は NULL
が入っているデータがないか、またそのカラムを使った検索処理を実行していないかチェックしておきましょう。
もしかしたら取得すべきデータを取得できていないかもしれませんよ?
まとめ
今回説明したような内容はデータベースのことがよく分かっている人でないと気づきにくい問題です。
変な不具合を埋め込んでしまわないようにboolean型のカラムを追加するときは必ずデフォルト値を設定するようにしましょう。
・・・といっても、人間はつい忘れちゃうのでマイグレーションを本番環境で実行する前に 誰かにコードレビューしてもらう ことをオススメします。
また、Boolean型に限らず NULL
はいろいろとやっかいな問題を引き起こすので、データベースとNULL
の関係はしっかり勉強しておいた方がいいですね。