Room 2.x を使用してローカル データベースにデータを保存する

アプリで比較的大量の構造化データを処理する場合は、そのデータをローカルに永続化することで大きなメリットを得ることができます。最も一般的なユースケースは、デバイスがネットワークにアクセスできない場合でも、ユーザーがオフラインの間にコンテンツをブラウジングできるように、関連するデータをキャッシュに保存することです。

Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、データベースへのスムーズなアクセスを可能にし、SQLite を最大限に活用できるようにします。

Room 2.x を設定する

アプリで Room 2.x を使用するには、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {
    val room_version = "2.6.1"

    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // To use Kotlin Symbol Processing (KSP)
    // ksp("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
}

主要コンポーネント

Room には 3 つの主要なコンポーネントがあります。

  • データベースを保持し、アプリの永続データに対する基礎的な接続のメイン アクセス ポイントとして機能するデータベース クラス
  • アプリのデータベースのテーブルを表すデータ エンティティ
  • アプリがデータベースのデータのクエリ、更新、挿入、削除に使用できるメソッドを提供するデータ アクセス オブジェクト(DAO)

図 1 に、Room のさまざまなコンポーネントの関係を示します。

図 1.Room ライブラリのアーキテクチャの図。

実装例

// Entity
@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}

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

// Usage
val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

val userDao = db.userDao()
val users: List<User> = userDao.getAll()

エンティティを使用してデータを定義する

各 Room エンティティは、データベース内のテーブルを表します。各エンティティは、 クラスとして定義します。@Entity

@Entity(tableName = "users")
data class User (
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?,
    @Ignore val picture: Bitmap? = null
)
  • カスタムのテーブル名と列名: デフォルトでは、Room はクラス名を テーブル名として、プロパティ名を列名として使用します。これらをカスタマイズするには、 tableName プロパティを @Entity@ColumnInfo(name = "...") アノテーションで使用します。
  • 主キー: 主キーを定義するには、@PrimaryKey を使用します。複合キーの場合は、 primaryKeys プロパティを使用します@Entity@Entity(primaryKeys = ["firstName", "lastName"])
  • フィールドを無視する: フィールドが永続化されないようにするには、 @Ignore を使用します。

型コンバータ

Date などのカスタム型を単一の列に保存する必要が生じることがあります。@TypeConverter メソッドを指定して、カスタム型と Room が永続化できる型との間で変換します。

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

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

// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }

DAO を使用してデータにアクセスする

DAO は、データベース操作のメソッドを定義します。インターフェースまたは抽象 クラスに @Dao アノテーションを付けます。

コンビニエンス メソッド

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Update
    fun updateUsers(vararg users: User)

    @Delete
    fun deleteUsers(vararg users: User)
}
  • 挿入: insert メソッドは、挿入された行の ID を表す Long、または挿入されたすべての行の ID を含む List<Long> を返すことができます。
  • 更新または削除: update メソッドまたは delete メソッドは、影響を受けた行数を表す Int を返すことができます。

クエリメソッド

メソッドに @Query アノテーションを付けて、SQL ステートメントを作成します。Room はコンパイル時にクエリを検証します。

@Dao
interface UserDao {
    // Simple query
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>

    // Return a subset of columns using a POJO or tuple
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(): List<NameTuple>

    // Pass parameters
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>

    // Collection of parameters
    @Query("SELECT * FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<User>

    // Join tables
    @Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
    fun findBooksBorrowedByName(userName: String): List<Book>
}

マルチマップの戻り値の型

Room 2.4 以降では、クエリメソッドは Map 型を使用してマルチマップを直接返すことができます。

@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>

非同期 DAO クエリ

UI のフリーズを避けるため、データベース クエリはメインスレッドで実行できません。次のいずれかのインテグレーションを使用して、クエリを非同期にします。

Kotlin コルーチンと Flow

room-ktx 依存関係が必要です。

@Dao
interface UserDao {
    // One-shot async query
    @Insert
    suspend fun insertUsers(vararg users: User)

    // Observable query using Flow
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>
}

Java と RxJava

room-rxjava2 または room-rxjava3 が必要です。

@Dao
interface UserDao {
    @Insert
    fun insertUsers(users: List<User>): Completable

    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flowable<User>
}

Java と LiveData、Guava

ListenableFuture には room-guava が必要です。

@Dao
interface UserDao {
    // LiveData for observable queries
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): LiveData<User>

    // Guava ListenableFuture for one-shot queries
    @Insert
    fun insertUsers(users: List<User>): ListenableFuture<Integer>
}

Room 2.x でリレーションを定義する

UI スレッドでの遅延読み込みを防ぐため、エンティティ間で直接オブジェクト参照を使用することはできません。代わりに、中間 データクラスで @Relation を使用してリレーションを定義します。

1 対 1

各ユーザーはライブラリを 1 つだけ持ちます。

@Entity
data class User(@PrimaryKey val userId: Long, val name: String)

@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)

// Intermediate class
data class UserAndLibrary(
    @Embedded val user: User,
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    val library: Library
)

// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>

1 対多

各ユーザーは複数のプレイリストを持つことができます。

@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)

data class UserWithPlaylists(
    @Embedded val user: User,
    @Relation(
          parentColumn = "userId",
          entityColumn = "userCreatorId"
    )
    val playlists: List<Playlist>
)

多対多

プレイリストには多数の曲を含めることができ、それぞれの曲は多数のプレイリストに含められます。ジャンクション テーブルが必要です。

@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

ネストされたリレーション

ユーザー、そのプレイリスト、それらのプレイリスト内のすべての曲をクエリします。

data class UserWithPlaylistsAndSongs(
      @Embedded val user: User,
      @Relation(
          entity = Playlist::class,
          parentColumn = "userId",
          entityColumn = "userCreatorId"
      )
      val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
  )

データベース管理

このセクションでは、データベース ビュー、データの事前入力、データベースの移行など、Room データベースの管理に関するさまざまな側面について説明します。

データベース ビュー

複雑なクエリを @DatabaseView アノテーションを付けたクラスにカプセル化します。

@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)

// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }

データベースを事前入力する

アセット ファイルまたはファイル システムから初期化時にデータベースにデータを入力します。

Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
    .createFromAsset("database/myapp.db")
    .build()

移行

スキーマを変更する場合は、データベースのバージョンを増やして Migration オブジェクトを定義します。

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

Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
  .addMigrations(MIGRATION_1_2)
  .build()
  • 自動移行: Room 2.4.0 以降を使用している場合は、 @AutoMigration を使用して基本的なスキーマ変更を自動的に移行できます。これを行うには、データベース構成で exportSchematrue に設定する必要があります: @Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)])
  • 破壊的なフォールバック: 移行パス がない場合にデータが失われることを許容できるのであれば、データベースの作成時に .fallbackToDestructiveMigration を呼び出します。

移行をテストする

移行を確認するには、room-testing アーティファクトの MigrationTestHelper を使用します。これをサポートするには、build.gradle 構成でスキーマをエクスポートしていることを確認してください。

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    fun migrate1To2() {
        var db = helper.createDatabase("test-db", 1).apply {
            execSQL("INSERT INTO User VALUES (1, 'John')")
            close()
        }
        db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
        // Verify data was migrated correctly
    }
}

SQLite から Room に移行する

アプリを SQLite から Room に移行するには、次の手順を完了します。

  1. Room を含めるように依存関係を更新 します。
  2. @Entity@PrimaryKey@ColumnInfo を使用してモデルクラスにアノテーションを付けます
  3. ヘルパークエリメソッドを置き換えるDAO を作成します
  4. エンティティと DAO を参照するRoomDatabase クラスを作成します 。バージョン番号を増やします。
  5. 空の移行パスを定義します。スキーマは変更されず、フレームワークのみが変更されるためです。kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. 移行パスで Room.databaseBuilder を使用するようにインスタンス化を更新します