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

# 何が起こった？

Ruby on Rails 5.1.6 で作っていたウェブアプリを 5.2.0 に上げた。
一通りの作業のあと，正常に動く状態になった（ように見えた）。

ところが検索機能が少しおかしい。
boolean 型のカラム（仮に `okay` というカラム名だとする）が

```rb
where(okay: true)
```

で検索しても

```rb
where(okay: false)
```

で検索しても 0 件になる。
このカラムはすべてのレコードで値が入っているはずだ。
なんで？

# 原因を調べる

まずは，データベースの中身をみよう。データベースは SQLite3 なので，SQLite3 ファイルが開けるアプリでテーブルを確認。
うん，確かに `okay` カラムは全部値が入っている。
具体的には，真のとき `"t"`，偽のとき `"f"` になっている。

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

次に調べることは，

```rb
where(okay: true)
```

で検索したときに，どんなクエリーが発行されるのか。
ターミナルに表示されるログを見ると

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

のようになっていた。
ちょっと待て！　`"t"` じゃないのか。`"1"` で検索したら，そりゃあ 0 件だわ。

# なんでこうなった？

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

ActiveRecord 5.2.0 の [CHANGELOG](https://github.com/rails/rails/blob/v5.2.0/activerecord/CHANGELOG.md) にしれっと書いてあった。

```
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` カラムの値を書き換えることにした。

しかし，

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

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

そこで，

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

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

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

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

# 追記

## 値を更新するマシな方法（2019-07-06）

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

つまり，

```rb
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` であったとする。
もし，

```rb
Item.create okay: false
```

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

```rb
Item.create
```

のように，`okay` の値を指定しなかった場合どうなるか。
発行される SQL の `INSERT` 文には `okay` カラムについては何も出力されない。
この場合，SQLite3 のテーブル定義に書かれた「デフォルト値」が使われる。

テーブルが作られたのが Rails 5.2 より前だった場合，SQLite3 のテーブル定義でデフォルト値は `'f'` になっているはず。
この状態だと，`okay` に値を指定せずにレコード作成すると，値は `0` でなく `'f'` になってしまう。
これは SQLite3 側で起こることなので，ActiveRecord からは手が出せない。

なので，テーブル定義がどうなっているかを必ず確認しよう。
`db/schema.rb` を見ても無駄である。そこには

```rb
    t.boolean "okay", default: false
```

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

なので，テーブル定義を確認するには SQLite3 に当たるしかない。
SQlite3 の中身を見るツールはいくつもあるので，それで見よう。そしたら

```
DEFAULT 'f'
```

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

ではこのデフォルト値を `0` に変えるにはどうするか？
やり方を書いたものは見つけられなかったが，あてずっぽうでやってみたら簡単に分かった。
新しいマイグレーションを作り，

```rb
change_column_default :items :okay, false
```

のようにして，改めてデフォルト値を設定してやればいいだけだった。
もともと `false` がデフォルトなのに，改めて `false` を指定して効いてくれるのかな？と思ったが，ちゃんとこれで SQLite3 としてのデフォルト値が `0` に変わってくれた。