Room Database:データベースファイル作成のコールバック

投稿日:  更新日:

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です。

Roomのプログラム構成

コールバックは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
        }
    }
}
補助関数
fun getThreadName(): String = Thread.currentThread().name
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単位なので、「事前データの取り込み」と「アプリの初回データ取得」がレーシング(競合)します。

スポンサーリンク

関連記事:

Androidアーキテクチャコンポーネント(AAC)は「堅牢でテストとメンテナンスが簡単なアプリの設計を支援する」とドキュメントで説明されています。 有効そうだけど、実態がよくわからないので、いろいろ調べて理解した内容をまとめました。 ...
Androidは標準でSQLiteというRDBMS(Relational Database Management System)を扱います。 SQLiteは少し個性的なデータベースです。 データベース全般の説明を通して、SQLiteと他の違いをまとめます。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 2     Kotlin 2.0.0 ...
データベースを扱うアプリケーションの開発で、動作を確認するためにデータベースの閲覧が出来ると便利です。 データベースは、マネージメントシステムを介してアクセスされるため、プログラムからブラックボックスに見えます。 ですので、データベース内部の問題は見つけ難いです。 デバックおいてデータベースの閲覧が出来れば、内部の問題を用意に確認できます。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 2     DB Browser for SQLite バージョン 3.13.1 ...
Androidが標準で扱うデータベースはSQLiteです。 ※詳細は「Androidで扱うデータベース」を参照 データベースを構築する方法は「SQLite API(Android SDK)」と「Room(Android Jetpack)」の2通りがあります。 今回は「Room」でデータベースを構築する方法を紹介します。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 2     Kotlin 2.0.0     androidx.sqlite:sqlite:2.4.0     androidx.room:room-*:2.6.1 ...
アプリケーションが始めて起動した直後に、データの事前取り込み(初期状態の設定)を必要とする場合があります。 事前取り込みの方法ついて、まとめました。 Room(Android Jetpack)でデータベースを構築した場合です。 ※環境: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(Android Jetpack)でデータベースを構築した場合、アプリのパフォーマンスを考えると、識別子の共有が必要になってきます。 この点について、まとめました。 ※環境: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でデータベースを構築」を参照 ...
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でデータベースを構築」を参照 ...
スポンサーリンク