Room(Android Jetpack)でデータベースを構築した場合、データベースへアクセスする際に、データベースのOpenとCloseという処理を伴います。
この点について、まとめました。
※環境:Android Studio Ladybug | 2024.2.1 Patch 3
Kotlin 2.0.0
androidx.sqlite:sqlite:2.4.0
androidx.room:room-*:2.6.1
※サンプルの全体像は「Roomでデータベースを構築」を参照
データベースのOpen
データベースのアクセスを開始する前に、データベースと接続の確立を行います。
この接続の確立を行う処理が「データベース(識別子)のOpen」に当たります。
データベース識別子は、その結果として得られるものです。
Room.databaseBuilder#build( )の実行は実装済みのRoomDatabaseインスタンス(識別子)を返します。ちなみに、RoomDatabaseは抽象関数(abstract)です。
この時、内部でデータベースのOpenが行われています。
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
abstract fun playerDao(): PlayerDao
companion object {
@Volatile
private var instance: GameDb? = null
fun getDatabase(context: Context): GameDb {
return instance ?: synchronized(this) {
val _instans = Room.databaseBuilder(
context,
GameDb::class.java, // 自身のクラス
"Game.db" // データベース名
)
.build()
instance = _instans
_instans
}
}
}
}
ただし、RoomのデータベースのOpenは、ユーザ側から見えている部分に過ぎません。

RoomはSQLite APIのラッパーAPIであって、内部でSQLite APIを呼び出しています。
つまり、マネージメントシステムとやり取り(問い合わせ⇔返答)を行っているのは、SQLite APIです。
ですので、内部側でSQLite APIデータベースのOpenが行われています。
RoomとSQLite APIデータベースのOpenのタイミングは異なります(黄色のイベント)。注意が必要です。
データベースのClose
データベースの為に確保されたリソース(主にメモリー)を開放します。
Closeは関数RoomDatabase#close( )で行います。
@Suppress("Deprecation") // Due to usage of `mDatabase`
open val isOpen: Boolean
get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true
...
/**
* Closes the database if it is already open.
*/
open fun close() {
if (isOpen) {
val closeLock: Lock = readWriteLock.writeLock()
closeLock.lock()
try {
invalidationTracker.stopMultiInstanceInvalidation()
openHelper.close() // ⇐ SQLiteOpenHelper#close( )が呼ばれる
} finally {
closeLock.unlock()
}
}
}
※mDatabase:SupportSQLiteDatabaseのインスタンス
※openHelper:SupportSQLiteOpenHelperのインスタンス
SuppoerXXXはRoomからSQLite APIへ関数の実行(ここではclose)を橋渡しするクラスです。
ですので、SQLite APIのデータベースのCloseと同様に、SQLiteOpenHelper#close( )が呼ばれることになります。
※詳細は「SQLite API Database:データベース(識別子)のOpenとClose」を参照

CloseされるのはSQLite APIの識別子であって、Roomの識別子はCloseされません。
注意点1:Close後の識別子は使用不可
Close後の識別子は使用できません。使用した場合はエラーになり、アプリが落ちます。
var _gameDb = GameDb.getDatabase(this@MainActivity)
Log.i(TAG, "RoomDatabase(D-0) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb.playerDao().insert(Player(0, "RRRRRRRR", 123))
Log.i(TAG, "RoomDatabase(D-1) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb.close()
Log.i(TAG, "RoomDatabase(D-2) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb = GameDb.getDatabase(this@MainActivity)
Log.i(TAG, "RoomDatabase(D-3) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb.playerDao().insert(Player(0, "SSSSSSSS", 456))
Log.i(TAG, "RoomDatabase(D-4) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
RoomDatabase(D-0) = 22493922, isOpen:false RoomDatabase(D-1) = 22493922, isOpen:true // アクセス時にSQLite APIの識別子がOpenされる RoomDatabase(D-2) = 22493922, isOpen:false // SQLite APIの識別子がCloseされる RoomDatabase(D-3) = 22493922, isOpen:false ... java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed. ...
注意点2:Close後の識別子を再Open
Close後の識別子を再Openするには、Roomの識別子を再作成するしかありません。
この為に、シングルトン化で行われた自己保持(instance変数)を解除する処理が必要になります。
※シングルトン化については「Room Database:識別子(RoomDatabase)の共有」を参照
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
abstract fun playerDao(): PlayerDao
companion object {
@Volatile
private var instance: GameDb? = null
fun getDatabase(context: Context): GameDb {
return instance ?: synchronized(this) {
val _instans = Room.databaseBuilder(
context,
GameDb::class.java,
"Game.db"
)
.build()
instance = _instans
_instans
}
}
fun closeDatabase() {
instance?.close()
instance = null
}
}
}
var _gameDb = GameDb.getDatabase(this@MainActivity)
Log.i(TAG, "RoomDatabase(D-0) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb.playerDao().insert(Player(0, "RRRRRRRR", 123))
Log.i(TAG, "RoomDatabase(D-1) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
GameDb.closeDatabase()
Log.i(TAG, "RoomDatabase(D-2) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb = GameDb.getDatabase(this@MainActivity)
Log.i(TAG, "RoomDatabase(D-3) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
_gameDb.playerDao().insert(Player(0, "SSSSSSSS", 456))
Log.i(TAG, "RoomDatabase(D-4) = ${_gameDb.hashCode()}, isOpen:${_gameDb.isOpen}")
RoomDatabase(D-0) = 18034992, isOpen:false RoomDatabase(D-1) = 18034992, isOpen:true RoomDatabase(D-2) = 18034992, isOpen:false RoomDatabase(D-3) = 26510877, isOpen:false // Roomの識別子の再作成 RoomDatabase(D-4) = 26510877, isOpen:true // アクセス時にSQLite APIの識別子がOpenされる
ただし、Openは多くのコスト(処理時間、CPUの能力消費、メモリー消費)を必要とします。
小まめなCloseとOpenの繰り返しは、アプリケーション全体のパフォーマンス低下を引き起こす可能性があるので、行わない実装にすべきです。
Closeのタイミング
識別子のCloseは、問題点1,2を引き起こす可能性があります。エラー時は「アプリが落ちる」という、非常に深刻な状況です。
ですので、アプリケーションの起動している間は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>>(listOf<Player>())
}
override fun onDestroy() {
super.onDestroy()
gameDb.close()
}
}
関連記事:
