🏠

【Android】Jetpack Room導入~基本的な使い方~シードデータ登録まで

2023/11/12に公開

はじめに

今回はJetpack Roomの導入方法から基本的な使い方、シードデータを登録するサンプルまでをやっていきたいと思います。

Jetpack Roomとは?

Jetpack Roomは、Android Jetpackの一部である永続データベースライブラリです。RoomはSQLiteを対象とした抽象化レイヤで、データベースのセットアップ、設定、クエリを行うための便利なAPIが用意されています。これにより、Androidアプリがデータベースとやりとりする際の処理を自動化することが可能になります

Roomの最新バージョン

AndroidX Tech: androidx.room:room-ktx

↑こちらから確認できます。

セットアップ

Ksp gradle plugin導入

gradle/libs.versions.toml に以下を追加

[versions]
ksp = "1.9.0-1.0.13"

[plugins]
ksp-gradle-plugin = { id = "com.google.devtools.ksp", version.ref = "ksp" }

org.jetbrains.kotlin.android pluginが古いと怒られるので 1.9.10 以上にしておきます。

app/build.gradle.kts に以下を追加します。

plugins {
    alias(libs.plugins.ksp.gradle.plugin) // 追加
}

Room関連パッケージ導入

gradle/libs.versions.toml に以下を追加します。

[versions]
room = "2.6.0"
lifecycle-runtime-ktx = "2.6.2" # lifecycleScopeを扱うため追加

[libraries]
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
# room
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

app/build.gradle.kts に以下を追加します。

dependencies {
    implementation(libs.lifecycle.runtime.ktx)
    // room
    implementation(libs.room.runtime)
    implementation(libs.room.ktx)
    annotationProcessor(libs.room.compiler)
    ksp(libs.room.compiler)
}

主要コンポーネント

早速基本的な使い方を実装するに当たり、以下の主要コンポーネントをそれぞれ実装していく形になります。

image1.png

  • Room エンティティ
    • 保存するオブジェクトを表すようにエンティティを定義します
    • 各エンティティは、関連付けられた Room データベース内のテーブルに対応し、エンティティの各インスタンスは、対応するテーブルのデータ行を表します
  • Room DAO
    • データアクセス オブジェクト(DAO)を定義して、保存対象のデータを操作します
    • 各 DAO は、アプリのデータベースへの抽象アクセスを可能にするメソッドを備えています
    • コンパイル時に定義した DAO の実装を自動的に生成します
  • Room Database クラス
    • データベースを保持し、アプリの永続データに対する基礎的な接続のメインアクセスポイントとして機能します

Entity実装

今回は User Entityを作成してみたいと思います。 app/data/user 配下に User.ktを以下内容で作成します。

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    var name: String = "",
    var image: String
)

DAO実装

先程作成した User EntityのDAOを作成してみます。app/data/user 配下に UserDao.ktを以下内容で作成します。

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDao {
    @Query("SELECT * from users WHERE id = :id")
    fun getUser(id: Int): Flow<User>

    @Insert
    suspend fun insert(user: User)
}

一旦id指定してUser取得と登録だけできるDaoを作成してます。

Database実装

Entity と DAO を使用する [RoomDatabase](https://developer.android.com/reference/androidx/room/RoomDatabase?hl=ja) を作成します

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var Instance: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
                    .fallbackToDestructiveMigration()
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

@Volatile アノテーションを付け、*synchronized*で囲むことでスレッドセーフにしてます。

fallbackToDestructiveMigration を付けるとマイグレーションに失敗したらデータ削除してDatabaseを再構築します。

試しに実行してみます。実際のプロダクションコードでは使えませんが、一度限りの確認用の以下コードを MainActivityonCreate に追加し、動かしてみるとLogcatに登録されたユーザーが表示されるかと思います。

val db = AppDatabase.getDatabase(applicationContext)
val repository: UserRepository = UserRepository(db.userDao())
val user = User(name = "Hoge", image = "Image")
lifecycleScope.launch {
    repository.insertUser(user)
    repository.getUsers().collect {
        Timber.d("user: $it")
    }
}

シードデータ登録

次にDB作成後に一度だけデータを登録するような処理を実装したいと思います。

databaseBuilderaddCallback でコールバックを追加し、onCreateをoverrideしその中でシードデータを登録するようにします。

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var Instance: AppDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, AppDatabase::class.java, "rediary_database")
                    .fallbackToDestructiveMigration()
                    .addCallback(seedCallback(context, scope))
                    .build()
                    .also { Instance = it }
            }
        }

        private fun seedCallback(context: Context, scope: CoroutineScope): Callback {
            return object : Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)
                    Instance?.let {
                        scope.launch(Dispatchers.IO) {
                            val repository: UserRepository = UserRepository(it.userDao())
                            val user = User(name = "Hoge", image = "Image")
                            repository.insertUser(user)
                        }
                    }
                }
            }
        }
    }
}

上記の例では seedCallback を登録し、DBが作成されたタイミングで渡された CoroutineScopeDispatchers.IO で起動しシードデータを登録しています。

MainActivity側で先程のコードを以下に修正します。

val db = AppDatabase.getDatabase(applicationContext, lifecycleScope)
val repository: UserRepository = UserRepository(db.userDao())
lifecycleScope.launch {
    repository.getUsers().collect {
        Timber.d("user: $it")
    }
}

getDatabaselifecycleScope を渡すように修正しています。実際はその時々の CoroutineScope を使うようになるかと思います。

他にもいいやり方があるかもですが今回はこの方法を試してみました。

また、コールバックの onCreate が呼び出されるタイミングとしては、実際にQueryやInsertなど処理が走った初回時に呼び出されます。

参考URL

Discussion