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() } }
関連記事: