何が起こった?
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_all
は updated_at
カラムの値を更新しない,という点でもありがたい。
デフォルト値の落とし穴(2019-07-06)
もう一つ大事なことがある。
Rails 5.2 より前に作ったアプリを 5.2 にアップデートして,boolean の true
/false
が 1
/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
のようにしか書かれていないから。そりゃそうだよね,ここにはデフォルト値が true
か false
かが書かれるべきであって,SQLite3 という特定のデータベースシステムでどうなるかは書かれるべきでないから。
なので,テーブル定義を確認するには SQLite3 に当たるしかない。
SQlite3 の中身を見るツールはいくつもあるので,それで見よう。そしたら
DEFAULT 'f'
のようになっているはずだ。
ではこのデフォルト値を 0
に変えるにはどうするか?
やり方を書いたものは見つけられなかったが,あてずっぽうでやってみたら簡単に分かった。
新しいマイグレーションを作り,
change_column_default :items :okay, false
のようにして,改めてデフォルト値を設定してやればいいだけだった。
もともと false
がデフォルトなのに,改めて false
を指定して効いてくれるのかな?と思ったが,ちゃんとこれで SQLite3 としてのデフォルト値が 0
に変わってくれた。