tech

AndroidのRoomで始めるローカル永続化

· 15min · Android, Kotlin, Room
index

はじめに

Androidでローカルにデータを永続化したいとき、直接SQLiteを叩くとボイラープレートが多すぎて辛いですよね。 そこで活躍するのがJetpackの一部として提供されているRoomです。

Roomは「SQLiteの薄いラッパー」ではなく、アノテーションベースで型安全なDBアクセスを提供してくれるライブラリ。 コンパイル時にクエリの整合性まで検査してくれるので、かなりミスが減ります。

本記事では、Roomの基本的な使い方をまとめます。

依存関係

dependencies {
    val roomVersion = "2.5.2"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    ksp("androidx.room:room-compiler:$roomVersion")
}

kaptよりも高速なkspが主流になってきているので、新規プロジェクトはkspでOKです。

Roomの3つの構成要素

Roomは以下の3つの役割でDBを構成します。

要素役割
Entityテーブルとなるデータクラス
DAOSQLを発行するインターフェース
DatabaseEntity/DAOをまとめるDB本体

Entityの定義

@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val email: String,
)
  • @Entityでテーブルを表す
  • @PrimaryKeyで主キーを指定
  • autoGenerate = trueにしておくとIDを自動採番

DAOの定義

DAO (Data Access Object) はCRUD操作を定義するインターフェースです。

@Dao
interface UserDao {

    @Query("SELECT * FROM users ORDER BY id DESC")
    fun observeAll(): Flow<List<UserEntity>>

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun findById(id: Long): UserEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(user: UserEntity)

    @Delete
    suspend fun delete(user: UserEntity)
}

Flowを戻り値にしておくと、DBの変更をリアルタイムに監視できます。 Composeと組み合わせた時の体験が良いです。

Databaseクラス

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

シングルトンとして提供する

object DatabaseProvider {
    @Volatile private var instance: AppDatabase? = null

    fun get(context: Context): AppDatabase {
        return instance ?: synchronized(this) {
            instance ?: Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app.db",
            ).build().also { instance = it }
        }
    }
}

実務ではHiltでDIしてしまう方が多いですが、まずはシングルトンで動かしてみるとイメージがつきやすいです。

実際に使ってみる

class UserRepository(private val dao: UserDao) {
    fun observeUsers(): Flow<List<UserEntity>> = dao.observeAll()

    suspend fun addUser(name: String, email: String) {
        dao.upsert(UserEntity(name = name, email = email))
    }
}

ViewModelからはこうです。

class UserViewModel(
    private val repository: UserRepository,
) : ViewModel() {

    val users: StateFlow<List<UserEntity>> =
        repository.observeUsers().stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )

    fun add(name: String, email: String) {
        viewModelScope.launch {
            repository.addUser(name, email)
        }
    }
}

マイグレーション

テーブルに列を追加したい、といった変更にはマイグレーションが必要です。

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()

破壊的で良いならfallbackToDestructiveMigration()で全データを消してマイグレーション…という手もありますが、本番では避けましょう。

Type Converter

DateEnumをそのままEntityに持たせたい場合は、Type Converterを使います。

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time
}

@Database(
    entities = [UserEntity::class],
    version = 2,
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { /* ... */ }

おわりに

Roomは一度セットアップしてしまえば、型安全・Flow対応・マイグレーションと至れり尽くせりのDBライブラリです。 個人開発でもちょっとしたキャッシュ層として使えるので、SQLiteを直接叩くくらいならぜひRoomを試してみてください!

参考文献