使用 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 有三个主要组件:

  • 数据库类 ,用于保存数据库并作为应用持久性数据底层连接的主要访问点。
  • 数据实体 ,用于表示应用的数据库中的表。
  • 数据访问对象 (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 方法可以返回一个 Long,表示插入的行 ID;也可以返回一个 List<Long>,其中包含所有插入行的 ID。
  • 更新或删除: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 查询

为避免界面冻结,数据库查询无法在主线程上运行。使用以下集成之一,使查询异步运行:

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-rxjava2room-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

需要 room-guava 才能使用 ListenableFuture

@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 中定义关系

为防止在界面线程上延迟加载,您无法在实体之间使用直接对象引用。而是使用带有 @Relation 的中间 数据类定义关系。

一对一

每个用户只有一个库。

@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>

一对多

每个用户可以有多个播放列表。

@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 自动迁移基本架构更改。这要求您在数据库配置中将 exportSchema 设置为 true@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. 创建 RoomDatabase 类 ,引用实体和 DAO。 递增版本号。
  5. 定义空迁移路径,因为架构不会更改,只会更改 框架: kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. 更新实例化 ,以使用带有迁移路径的 Room.databaseBuilder