Edited at

Rails 5.2 で SQLite3 の boolean の値が変わった


何が起こった?

Ruby on Rails 5.1.6 で作っていたウェブアプリを 5.2.0 に上げた。

一通りの作業のあと,正常に動く状態になった(ように見えた)。

ところが検索機能が少しおかしい。

boolean 型のカラム(仮に okay というカラム名だとする)が

where(okay: true)

で検索しても

where(okay: false)

で検索しても 0 件になる。

このカラムはすべてのレコードで値が入っているはずだ。

なんで?


原因を調べる

まずは,データベースの中身をみよう。データベースは SQLite3 なので,SQLite3 ファイルが開けるアプリでテーブルを確認。

うん,確かに okay カラムは全部値が入っている。

具体的には,真のとき "t",偽のとき "f" になっている。

ちょっと補足すると,SQLite3 は boolean という型は持っていない。Rails(というか ActiveRecord)が true/false"t"/"f" に変えて保存したり検索したりしてくれる。

次に調べることは,

where(okay: true)

で検索したときに,どんなクエリーが発行されるのか。

ターミナルに表示されるログを見ると

WHERE "items"."okay" = ?  [["okay", "1"]]

のようになっていた。

ちょっと待て! "t" じゃないのか。"1" で検索したら,そりゃあ 0 件だわ。


なんでこうなった?

Rails(というか ActiveRecord)の 5.1.6 → 5.2.0 で何かが変わったぽいので,更新履歴を見てみよう。

ActiveRecord 5.2.0 の CHANGELOG にしれっと書いてあった。

Change sqlite3 boolean serialization to use 1 and 0.

SQLite natively recognizes 1 and 0 as true and false, but does not natively recognize 't' and 'f' as was previously serialized.

This change in serialization requires a migration of stored boolean data for SQLite databases, so it's implemented behind a configuration flag whose default false value is deprecated.

Lisa Ugray

とある。

よく分からないけど,ともかく 5.2.0 からは "t"/"f" じゃなくて 1/0 でいくことになったようだ。


どうすればいい?

とりあえず okay カラムの値を書き換えることにした。

しかし,

Item.all.each{ |item| item.update(okay: item.okay) }

だとレコードが書き変わらない。たぶん,等価な値への変更は保存しない仕組みなんだろう。

そこで,

Item.all.each{ |item| item.update(okay: !item.okay).update(okay: !item.okay) }

みたいな,論理反転を二度繰り返すワケの分からないコードで対処した。

しかし,こんなことをしたのでは,updated_at カラムが書き変わってしまう。今回はそれでも問題なかったが,困るケースもあるだろう。うーむ。

よい方法があれば教えてください。


追記


値を更新するマシな方法(2019-07-06)

「どうすればいい?」の節に書いたよりマシな方法。検索や更新を true, false でなく 't', 'f'0, 1 でやればいいのだった。

つまり,

Item.where("okay = 't'").update_all(okay: 1)

Item.where("okay = 'f'").update_all(okay: 0)

この方法はシンプルだし,なにより update_allupdated_at カラムの値を更新しない,という点でもありがたい。


デフォルト値の落とし穴(2019-07-06)

もう一つ大事なことがある。

Rails 5.2 より前に作ったアプリを 5.2 にアップデートして,boolean の true/false1/0 で記録されるようになったとしても,依然として 't'/'f' が書き込まれてしまうケースがあるのだ

それは,boolean なカラムに値を指定せずレコードを作ったような場合。

Item モデルに okay という boolean なカラムがあり,そのデフォルト値が false であったとする。

もし,

Item.create okay: false

のようにすれば,ActiveRecord は SQLite3 のレコードに 0 を書き込んでくれるだろう。

しかし

Item.create

のように,okay の値を指定しなかった場合どうなるか。

発行される SQL の INSERT 文には okay カラムについては何も出力されない。

この場合,SQLite3 のテーブル定義に書かれた「デフォルト値」が使われる。

テーブルが作られたのが Rails 5.2 より前だった場合,SQLite3 のテーブル定義でデフォルト値は 'f' になっているはず。

この状態だと,okay に値を指定せずにレコード作成すると,値は 0 でなく 'f' になってしまう。

これは SQLite3 側で起こることなので,ActiveRecord からは手が出せない。

なので,テーブル定義がどうなっているかを必ず確認しよう。

db/schema.rb を見ても無駄である。そこには

    t.boolean "okay", default: false

のようにしか書かれていないから。そりゃそうだよね,ここにはデフォルト値が truefalse かが書かれるべきであって,SQLite3 という特定のデータベースシステムでどうなるかは書かれるべきでないから。

なので,テーブル定義を確認するには SQLite3 に当たるしかない。

SQlite3 の中身を見るツールはいくつもあるので,それで見よう。そしたら

DEFAULT 'f'

のようになっているはずだ。

ではこのデフォルト値を 0 に変えるにはどうするか?

やり方を書いたものは見つけられなかったが,あてずっぽうでやってみたら簡単に分かった。

新しいマイグレーションを作り,

change_column_default :items :okay, false

のようにして,改めてデフォルト値を設定してやればいいだけだった。

もともと false がデフォルトなのに,改めて false を指定して効いてくれるのかな?と思ったが,ちゃんとこれで SQLite3 としてのデフォルト値が 0 に変わってくれた。