SQLite API(Android SDK)でデータベースを構築した場合、データベースへアクセスする際に、データベースのOpenとCloseという処理を伴います。
この点について、まとめました。
※環境:Android Studio Ladybug | 2024.2.1 Patch 2
Kotlin 2.0.0
※サンプルの全体像は「SQLite APIでデータベースを構築」を参照
目次
データベースのOpen
データベースのアクセスを開始する前に、データベースと接続の確立を行います。
この接続の確立を行う処理が「データベース(識別子)のOpen」に当たります。
データベース識別子は、その結果として得られるものです。
SQLiteOpenHelper(GameDb)はプロパティwritableDatabaseに識別子を保持します。プロパティの参照を行えば、識別子が取り出されます。
この時、内部でデータベースのOpenが行われています。
private SQLiteDatabase mDatabase;
...
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}
...
private SQLiteDatabase getDatabaseLocked(boolean writable) {
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
// Darn! The user closed the database by calling mDatabase.close().
mDatabase = null;
} else if (!writable || !mDatabase.isReadOnly()) {
// The database is already open for business.
return mDatabase;
}
}
... // データベースをオープンする処理
}
...

データベースのClose
データベースの為に確保されたリソース(主にメモリー)を開放します。
Closeは関数SQLiteOpenHelper#close( )で行います。
private SQLiteDatabase mDatabase;
...
public synchronized void close() {
if (mIsInitializing) throw new IllegalStateException("Closed during initialization");
if (mDatabase != null && mDatabase.isOpen()) {
mDatabase.close();
mDatabase = null;
}
}
...
注意点1:Close後の識別子は使用不可
Close後の識別子は使用できません。使用した場合はエラーになり、アプリが落ちます。
val _gameDb = GameDb(this, 1)
val _dbObj = _gameDb.writableDatabase
_dbObj.insert(tableName, null, values)
_gameDb.close()
_dbObj.insert(tableName, null, values)
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: データベース名
注意点2:Close後の識別子を再Open
同じSQLiteOpenHelperインスタンスから取り出される識別子は、同じインスタンスになります。つまり、シングルトンです。
Closeを行うと、識別子の機能は失われます。その後、識別子を参照した際に、再びOpenが行われます。
val _gameDb = GameDb(this, 1)
Log.i(TAG, "SQLiteDatabase(A) = ${_gameDb.writableDatabase.hashCode()}")
Log.i(TAG, "SQLiteDatabase(B) = ${_gameDb.writableDatabase.hashCode()}")
close()
Log.i(TAG, "SQLiteDatabase(C) = ${_gameDb.writableDatabase.hashCode()}")
SQLiteDatabase(A) = 250998571 // Open⇒新期作成の識別子 SQLiteDatabase(B) = 250998571 // (A)と同じ識別子 SQLiteDatabase(C) = 117358060 // Open⇒新規作成の識別子
ですので、次のような使い方はエラーになりません。再びOpenされるからです。
val _gameDb = GameDb(this, 1)
_gameDb.writableDatabase.insert(tableName, null, values) // 初回Open
_gameDb.close()
_gameDb.writableDatabase.insert(tableName, null, values) // 再びOpen
ただし、Openは多くのコスト(処理時間、CPUの能力消費、メモリー消費)を必要とします。
小まめなCloseとOpenの繰り返しは、アプリケーション全体のパフォーマンス低下を引き起こす可能性があるので、行わない実装にすべきです。
注意点3:複数スレッドでClose
データベースへのアクセスはスレッドセーフです。
異なるスレッドから同時刻にSQLが送られて来ても、マネージメントシステムが正しく処理できるように調整してくれます。

ただし、マネージメントシステムの処理が終わる前に、識別子をクローズしてしまうと、エラーになります。

Closeのタイミング
識別子のCloseは、問題点1,2,3を引き起こす可能性があります。エラー時は「アプリが落ちる」という、非常に深刻な状況です。
ですので、アプリケーションの起動している間はCloseを行いません。
明示的にCloseを行わなかったとしても、アプリケーションのプロセスが終了する時に、全てのリソースは開放されます。その開放に任せれば良いです。
※昔は未Closeを継続すると、ワーニングを出していたと思うのですが…
あえて明示的に行いたいのであれば、アプリケーションコンポーネントのトップに当たるMainActivity#onDestroyで行います。アプリケーションが終了に達していると、考えられるためです。

class MainActivity : ComponentActivity() {
private lateinit var gameDb: GameDb
private lateinit var playerFlow: MutableStateFlow<List<Player>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
gameDb = (application as MyApplication).gameDb
playerFlow = MutableStateFlow<List<Player>>(gameDb.fetchTop10())
}
override fun onDestroy() {
super.onDestroy()
gameDb.close()
}
}
関連記事:
