LoginSignup
10
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-05-17

何が起こった?

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 に変わってくれた。

10
6
1

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
10
6