LoginSignup
361

More than 5 years have passed since last update.

Boolean型のカラムを追加するときは必ずデフォルト値を設定しよう

Last updated at Posted at 2015-06-09

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: falseNULL (Rubyでいうところの nil)が設定されることを禁止するオプションです。
必須ではありませんが、NULL が入ると面倒な問題を引き起こしやすいので基本的に付けておくことをオススメします。

それではこの件に関する詳しい内容を以下で説明していきます。

データベースとRubyで異なる NULL / nil の扱い

デフォルト値を明示的に設定しなかった場合、新しいカラムには NULL が入ります。

データベース(SQL)の世界において、NULL は特殊な存在です。
NULLfalse の意味でもなく、「空(から)」の意味でもありません。
NULL「まだ決まっていないから何も評価できない」 の意味になります。(NULL の意味づけは文献によって多少異なるので、適宜ネットや技術書も参考にしてください)

つまり、NULLtrue でも false でもない状態」 を表します。
よって、Boolean型のカラムに NULL が入ると、「true / false / NULL」の3つの状態が存在することになるのです。

一方、Rubyでは nilfalse はどちらも偽」 として扱われます。

このようにデータベースと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さんは「メール通知を受け取る」設定にします。

Screen Shot 2015-06-10 at 4.38.51.png

Charlie さんは「メール通知を受け取る」を変更せずに、自分の名前だけを変更しました。(それと同時に notification_allowedfalse で更新されます)

Screen Shot 2015-06-10 at 4.39.21.png

その結果、データはこのようになりました。

 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 は入ってこないので、検索する場合も truefalse のどちらかを指定するだけで済みます。

# 「通知を受け取るユーザ」を検索
User.where(notification_allowed: true)

# 「通知を受け取らないユーザ」を検索
User.where(notification_allowed: false)

# NULLのデータは絶対に存在しないので実行結果は常に0件
User.where(notification_allowed: nil)

NULL は false と見なして検索する方法

「気づくのが遅かった!すでにデータに NULL が入っちゃってます!」という場合は以下のようにすれば NULLfalse のデータを同時に検索できます。
NULLfalse として扱う場合)

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の関係はしっかり勉強しておいた方がいいですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
361