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単位なので、「事前データの取り込み」と「アプリの初回データ取得」がレーシング(競合)します。
関連記事:
