Roomは処理の開始時にコールバックを受け取ることができます。
このコールバックついて、まとめました。
※環境:Android Studio Ladybug | 2024.2.1 Patch 3
Kotlin 2.0.0
androidx.sqlite:sqlite:2.4.0
androidx.room:room-*:2.6.1
DB Browser for SQLite バージョン 3.13.1
※サンプルの全体像は「Roomでデータベースを構築」を参照
目次
コールバックの受け取り
Roomは開始時に、2つのコールバックRoomDatabase.Callback#onCreateと#onOpenを受け取ることが出来ます。
タイミング | スレッド | ||
---|---|---|---|
onCreate | アプリの起動後に初めて データベースアクセスが 行われた時 | アプリの起動後に初めて データベースファイルが 作成された時 | データの登録・検索を 行ったスレッド |
onOpen | アプリの起動後に初めて データベースファイルが 開かれた時 |
||
※RoomDatabaseを取り出したタイミングと異なる |
@Database(entities = arrayOf(Player::class), version = 1) abstract class GameDb : RoomDatabase() { abstract fun playerDao(): PlayerDao // Daoの提供(必須) 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" // データベース名(ファイル名) ) .addCallback(object : Callback() { // データベースファイルが作成された時 override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) Log.i(TAG, "DB onCreate ! [${getThreadName()}]") } // データベースファイルが開かれた時 override fun onOpen(db: SupportSQLiteDatabase) { super.onOpen(db) Log.i(TAG, "DB onOpen ! [${getThreadName()}]") } }) .build() instance = _instans _instans } } } }
コールバックのタイミング
コールバックが呼び出されるタイミングは、データベースをオープンした(識別子RoomDatabaseを取り出した)時ではありません。
アプリの起動後に初めてデータベースアクセス(登録・検索など)が行われた時です。
この時、
データベースファイルが存在しなければ、作成し、onCreateを呼び出します。存在すれば、onCreateは呼ばれません。
その後、データベースファイルを開き、onOpenを呼び出します。
class RankingActivity : ComponentActivity() { private lateinit var gameDb: GameDb private lateinit var playerFlow: MutableStateFlow<List<Player>> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { ... } gameDb = (application as MyApplication).gameDb playerFlow = MutableStateFlow<List<Player>>(listOf<Player>()) lifecycleScope.launch(Dispatchers.Default) { val _list = gameDb.playerDao().fetchTopX(10) // 初めてアクセス playerFlow.value = _list } } }
RoomはSQLite APIのラッパーAPIであって、内部でSQLite APIを呼び出しています。
つまり、マネージメントシステムとやり取り(問い合わせ⇔返答)を行っているのは、SQLite APIです。
コールバックはSQLite APIの動作に連動しています。
SQLite APIはデータベースをオープンした(識別子SQLiteDatabaseを取り出した)時に、データベースファイルを作成したり、開いたりします。ですので、このタイミングでコールバックは呼び出されます。
コールバックのスレッド
コールバックを実行するスレッドは、引き金になったデータベースアクセスを実行したスレッドです。
class RankingActivity : ComponentActivity() { private lateinit var gameDb: GameDb private lateinit var playerFlow: MutableStateFlow<List<Player>> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { ... } gameDb = (application as MyApplication).gameDb playerFlow = MutableStateFlow<List<Player>>(listOf<Player>()) lifecycleScope.launch(Dispatchers.Default) { Thread.sleep(100) } // ...-worker-1 lifecycleScope.launch(Dispatchers.Default) { Thread.sleep(100) } // ...-worker-2 lifecycleScope.launch(Dispatchers.Default) { // ...-worker-3 Log.i(TAG, "First fetch ! [${getThreadName()}]") val _list = gameDb.playerDao().fetchTopX(10) // 初めてアクセス playerFlow.value = _list } } }
First fetch ! [DefaultDispatcher-worker-3] DB onCreate ! [DefaultDispatcher-worker-3] DB onOPen ! [DefaultDispatcher-worker-3]
データの事前取り込み
コールバックを用いて、データの事前取り込み(初期状態の設定)が出来ます。
しかし、制約があるのでお勧めしません。ファイルを用いる方法が最良であると思います。※詳細は「Room Database:データの事前取り込み」を参照
(1)引数db:SupportSQLiteDatabaseを利用
コールバックonCreateは引数にSupportSQLiteDatabaseを持ちます。
この識別子を用いれば、SQLite APIと同じ方法でSQLを発行し、データの事前取り込みを行うことができます。
※SupportSQLiteDatabase:SQLite APIのデータベース識別子(SQLiteDatabase)へ橋渡しする関数、SQLiteDatabaseとほぼ等価
※SQLite APIデータベースの事前取り込みについては「SQLite API Database:データの事前取り込み」を参照
@Database(entities = arrayOf(Player::class), version = 1) abstract class GameDb : RoomDatabase() { abstract fun playerDao(): PlayerDao // Daoの提供(必須) 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" // データベース名(ファイル名) ) .addCallback(object : Callback() { // データベースが作成された時 override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) repeat(10) { val _player = Player(0, "ZZZZZZZZ", 0) val _values = ContentValues().apply { put("name", _player.name) put("score", _player.score) } db.insert("Player", CONFLICT_NONE, _values) } } }) .build() instance = _instans _instans } } } }
ただし、Columnのフィールド名、テーブル名をソースコード中に埋め込む必要があります。
アノテーション@Daoからシンボルプロセッサにより、Dao関数群を自動生成する利点が半減してしまいます。
(2)Daoの利用(エラーで落ちる)
(1)を改善し、Dao関数を用いてデータの事前取り込みを行います。
@Database(entities = arrayOf(Player::class), version = 1) abstract class GameDb : RoomDatabase() { abstract fun playerDao(): PlayerDao // Daoの提供(必須) 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" // データベース名(ファイル名) ) .addCallback(object : Callback() { // データベースが作成された時 override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) instance?.let { val _dao = it.playerDao() repeat(10) { // 事前データの登録 _dao.insert(Player(-1, "ZZZZZZZZ", 0)) } } } }) .build() instance = _instans _instans } } } }
ただし、この方法は例外を発生して、アプリが落ちてしまいます。
原因は、スレッド内で処理がループする(赤字部分)からです。
(1)データベースアクセス(アプリの初回データ取得)
(2-1)識別子の取り出し(SQlite API) ⇐ 1回目のデータベースオープン
(2-2)データベースが無い⇒ファイル作成
(3)onCreateを実行
(4)データベースアクセス(事前データの登録)
(5-1)識別子の取り出し(SQlite API) ⇐ 2回目のデータベースオープン
(5-2)データベースが無い⇒ファイル作成
(6)例外発生 ⇒ データベースクローズ
FATAL EXCEPTION: DefaultDispatcher-worker-1 Process: パッケージ名, PID: 8287 java.lang.IllegalStateException: Closed during initialization at android.database.sqlite.SQLiteOpenHelper.close(SQLiteOpenHelper.java:452) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.innerGetDatabase(FrameworkSQLiteOpenHelper.kt:180) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getSupportDatabase(FrameworkSQLiteOpenHelper.kt:151) at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.kt:104) at androidx.room.RoomDatabase.inTransaction(RoomDatabase.kt:632) at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.kt:451) at パッケージ名.PlayerDao_GameDb_Impl.insert(PlayerDao_GameDb_Impl.java:65) at パッケージ名.db.GameDb$Companion$getDatabase$1$_instans$1.onCreate(GameDb.kt:43) ...
(3)Daoの利用+スレッド(レーシングを発生)
(2)を改善し、「事前データの取り込み」と「アプリの初回データ取得」を別スレッドに分離します。
異なるスレッドから同時刻にSQLが送られて来ても、マネージメントシステムが正しく処理できるように調整してくれます。
@Database(entities = arrayOf(Player::class), version = 1) abstract class GameDb : RoomDatabase() { abstract fun playerDao(): PlayerDao // Daoの提供(必須) companion object { @Volatile private var instance: GameDb? = null fun getDatabase(context: Context, scope: CoroutineScope): GameDb { return instance ?: synchronized(this) { val _instans = Room.databaseBuilder( context, GameDb::class.java, "Game.db" // データベース名(ファイル名) ) .addCallback(object : Callback() { // データベースが作成された時 override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) instance?.let { scope.launch(Dispatchers.Default) { val _dao = it.playerDao() repeat(10) { _dao.insert(Player(0, "ZZZZZZZZ", 0)) } } } } }) .build() instance = _instans _instans } } } }
ただし、このスレッド間の調整はSQL単位なので、「事前データの取り込み」と「アプリの初回データ取得」がレーシング(競合)します。
関連記事: