SQLiteを使う場合の注意点

さて、長いこと放置していたはてなダイアリーの方ですが、まとめ書きした方がいいものは、やっぱりこちらに書くということで。

AndroidSQLiteを使うケースは多々あると思いますが、明言されていない注意点があるので忘備録がてら。

SQLiteDatabase#closeは明示で呼ぶな、Cursor#closeは明示で呼べ

これはSQLiteの作りの話ですが、SQLiteではマルチスレッドに対してコネクションオープンからクローズまでは保障する、という作りになっています。
要はコネクション単位でスレッドセーフですよ、ということ。

AndroidSQLiteを使って検索系の処理をするのに、いわゆるWebアプリ的な作りで考えると、更新系処理ではCUD処理のあとにSQLiteDatabase#closeとしがちですが、android.database.sqlite.SQLiteException が発生するケースがあります。

どういったケースかというと、

  1. 検索系でSQLiteDatabase#queryでCursor取得
  2. 並行して更新系でSQLiteDatabase#close

というケース。

Cursorを取得しているときに、ちょうど更新系でcloseされていると、SQLITE_MISUSE (21) というエラーコード→SQLiteExceptionが返ります。
ぱっと見、NullPointerExceptionあたり?な感じもするかと思いますが、きちんとネイティブ層までコールされます。

なんでかというと、SQLiteDatabase#closeを呼ぶと、

  1. Java側のオブジェクト参照を解放
  2. JNI経由でネイティブコールし、dbclose処理
  3. SQLiteDatabase#mNativeHandleというメンバ変数をJNIコードで0初期化

します。文字通り、dbclose処理を行うわけです。

これにより、SQLiteDatabase#closeを呼んだオブジェクトはclose処理されます。んがしかし、これはあくまでメンバ変数です。
しかも、このオブジェクト自体は破棄されません。

これと並行してCursorを取っていると、問題が発生。Cursorもメンバ変数でSQLiteDatabaseを持ちます。
本来、SQLiteHelper#getReadableDatabase, getWritableDatabaseはSQLiteDatabase#isOpenがtrueだとなにもしませんが、falseだと新しく取得します。

つまり、ネイティブコールもするわけで。このとき、mNativeHandleはdbopen関数中で新しいポインタアドレスで更新されます。
こうなると、SQLiteDatabase#isOpenではtrueが返るのに、ポインタアドレスが異なる→SQLITE_MISUSE返る→SQLiteExceptionとなり、プロセスが終了しない限り例外が出続ける、という状況になります。
タイミングで発生するようになるわけですが、1度発生すると、ドハマリします。

解決はSQLiteDatabase#closeを呼ばないこと。SQLiteOpenHelper#closeも結局、SQLiteDatabase#closeにいくので同じです。
んじゃ、close処理はどーするの?って気になりますが、そこはSQLiteDatabase#finalizeがやってくれます。
つまるところ、最終的にはDalvikVMが破棄時にするので気にするな、ということになります。

ただし、Cursor#closeは呼んでおく必要があります。呼んでない場合、破棄時にERRORログがでます。
Read専用でCursorを使う限りはまだいいのですが、リソースを扱うのできちんと閉じましょう。