【Android】Jetpack Room導入~基本的な使い方~シードデータ登録まで
はじめに
今回はJetpack Roomの導入方法から基本的な使い方、シードデータを登録するサンプルまでをやっていきたいと思います。
Jetpack Roomとは?
Jetpack Roomは、Android Jetpackの一部である永続データベースライブラリです。RoomはSQLiteを対象とした抽象化レイヤで、データベースのセットアップ、設定、クエリを行うための便利なAPIが用意されています。これにより、Androidアプリがデータベースとやりとりする際の処理を自動化することが可能になります
- Room を使用してデータを永続化する | Android Developers
- Room を使用してローカル データベースにデータを保存する | デベロッパー向け Android | Android Developers
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)
}
主要コンポーネント
早速基本的な使い方を実装するに当たり、以下の主要コンポーネントをそれぞれ実装していく形になります。
-
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を再構築します。
試しに実行してみます。実際のプロダクションコードでは使えませんが、一度限りの確認用の以下コードを MainActivity
の onCreate
に追加し、動かしてみると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作成後に一度だけデータを登録するような処理を実装したいと思います。
databaseBuilder
の addCallback
でコールバックを追加し、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が作成されたタイミングで渡された CoroutineScope
を Dispatchers.IO
で起動しシードデータを登録しています。
MainActivity側で先程のコードを以下に修正します。
val db = AppDatabase.getDatabase(applicationContext, lifecycleScope)
val repository: UserRepository = UserRepository(db.userDao())
lifecycleScope.launch {
repository.getUsers().collect {
Timber.d("user: $it")
}
}
getDatabase
に lifecycleScope
を渡すように修正しています。実際はその時々の CoroutineScope
を使うようになるかと思います。
他にもいいやり方があるかもですが今回はこの方法を試してみました。
また、コールバックの onCreate
が呼び出されるタイミングとしては、実際にQueryやInsertなど処理が走った初回時に呼び出されます。
Discussion