8-Room持久性库

Room持久性库

概览

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

Room 包含 3 个主要组件:

  • 数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。

    使用 @Database 注释的类应满足以下条件:

    • 是扩展 RoomDatabase 的抽象类。
    • 在注释中添加与数据库关联的实体列表。
    • 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法。

    在运行时,您可以通过调用 [Room.databaseBuilder()](https://developer.android.com/reference/androidx/room/Room#databaseBuilder(android.content.Context, java.lang.Class, java.lang.String)) 或 [Room.inMemoryDatabaseBuilder()](https://developer.android.com/reference/androidx/room/Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)) 获取 Database 的实例。

  • Entity:表示数据库中的表。

  • DAO:包含用于访问数据库的方法。

Room架构图如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FdAI8CbD-1592585026074)(/Users/liubo/Desktop/笔记/Jetpack/room_architecture.png)]

示例

以下代码段包含具有一个实体和一个 DAO 的示例数据库配置。

User

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

UserDao

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

        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        fun loadAllByIds(userIds: IntArray): List<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
               "last_name LIKE :last LIMIT 1")
        fun findByName(first: String, last: String): User

        @Insert
        fun insertAll(vararg users: User)

        @Delete
        fun delete(user: User)
    }

AppDatabase

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

获取数据库实例:

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

注意:如果您的应用在单个进程中运行,则在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。

如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 AppDatabase 实例,就可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中的 AppDatabase 实例。

声明依赖项

    dependencies {
      def room_version = "2.2.3"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

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

      // optional - RxJava 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"

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

配置编译器选项

    dependencies {
      def room_version = "2.2.3"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

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

      // optional - RxJava 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"

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

以下代码段举例说明了如何配置这些选项:

    android {
        ...
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [
                        "room.schemaLocation":"$projectDir/schemas".toString(),
                        "room.incremental":"true",
                        "room.expandProjection":"true"]
                }
            }
        }
    }
    

使用实体定义数据

以下代码段展示了如何定义实体:

    @Entity
    data class User(
        @PrimaryKey var id: Int,
        var firstName: String?,
        var lastName: String?
    )

要保留某个字段,Room 必须拥有该字段的访问权限。您可以将某个字段设为公开字段,也可以为其提供 getter 和 setter。如果您使用 getter 和 setter 方法,则请注意,这些方法需遵循 Room 中的 JavaBeans 规范。

注意:实体可以具有空的构造函数(如果相应的 DAO 类可以访问保留的每个字段),也可以具有其参数包含的类型和名称与该实体中字段的类型和名称一致的构造函数。Room 还可以使用完整或部分构造函数,例如仅接收部分字段的构造函数。

使用主键

每个实体必须将至少 1 个字段定义为主键。即使只有 1 个字段,您仍然需要为该字段添加 @PrimaryKey 注释。此外,如果您想让 Room 为实体分配自动 ID,则可以设置 @PrimaryKeyautoGenerate 属性。如果实体具有复合主键,您可以使用 @Entity 注释的 primaryKeys 属性,如

以下代码段所示:

    @Entity(primaryKeys = arrayOf("firstName", "lastName"))
    data class User(
        val firstName: String?,
        val lastName: String?
    )

默认情况下,Room 将类名称用作数据库表名称。如果您希望表具有不同的名称,请设置 @Entity 注释的 tableName 属性,如以下代码段所示:

    @Entity(tableName = "users")
    data class User (
        // ...
    )

注意:SQLite 中的表名称不区分大小写。

tableName 属性类似,Room 将字段名称用作数据库中的列名称。如果您希望列具有不同的名称,请将 @ColumnInfo 注释添加到字段,如以下代码段所示:

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

忽略字段

默认情况下,Room 会为在实体中定义的每个字段创建一个列。如果某个实体中有您不想保留的字段,则可以使用 @Ignore 为这些字段注释,如以下代码段所示:

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        val lastName: String?,
        @Ignore val picture: Bitmap?
    )

如果实体继承了父实体的字段,则使用 @Entity 属性的 ignoredColumns 属性通常会更容易:

    open class User {
        var picture: Bitmap? = null
    }

    @Entity(ignoredColumns = arrayOf("picture"))
    data class RemoteUser(
        @PrimaryKey val id: Int,
        val hasVpn: Boolean
    ) : User()

提供表搜索支持

Room 支持多种类型的注释,可让您更轻松地搜索数据库表中的详细信息。

除非应用的 minSdkVersion 低于 16,否则请使用全文搜索。

支持全文搜索(FTS:Full-text Search)

如果您的应用需要通过全文搜索 (FTS) 快速访问数据库信息,请使用虚拟表(使用 FTS3 或 FTS4 SQLite 扩展模块)为您的实体提供支持。要使用 Room 2.1.0 及更高版本中提供的这项功能,请将 @Fts3 或 @Fts4 注释添加到给定实体,如以下代码段所示:

    // Use `@Fts3` only if your app has strict disk space requirements or if you
    // require compatibility with an older SQLite version.
    @Fts4
    @Entity(tableName = "users")
    data class User(
        /* Specifying a primary key for an FTS-table-backed entity is optional, but
           if you include one, it must use this type and column name. */
        @PrimaryKey @ColumnInfo(name = "rowid") val id: Int,
        @ColumnInfo(name = "first_name") val firstName: String?
    )

注意:启用 FTS 的表始终使用 INTEGER 类型的主键且列名称为“rowid”。如果是由 FTS 表支持的实体定义主键,则必须使用相应的类型和列名称。

如果表支持以多种语言显示的内容,请使用 languageId 选项指定用于存储每一行语言信息的列:

    @Fts4(languageId = "lid")
    @Entity(tableName = "users")
    data class User(
        // ...
        @ColumnInfo(name = "lid") val languageId: Int
    )

Room 提供了其他几个选项来定义由 FTS 支持的实体,包括结果排序、令牌生成器类型以及作为外部内容管理的表。如需详细了解这些选项,请参阅 FtsOptions 参考。

将特定列编入索引

如果您的应用必须支持不允许使用由 FTS3 或 FTS4 表支持的实体的 SDK 版本,您仍可以将数据库中的某些列编入索引,以加快查询速度。要为实体添加索引,请在 @Entity 注释中添加 indices 属性,以列出要在索引或复合索引中包含的列的名称。

以下代码段演示了此注释过程:

    @Entity(indices = arrayOf(Index(value = ["last_name", "address"])))
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        val address: String?,
        @ColumnInfo(name = "last_name") val lastName: String?,
        @Ignore val picture: Bitmap?
    )

有时,数据库中的某些字段或字段组必须是唯一的。您可以通过将 @Index 注释的 unique 属性设为 true 来强制实施此唯一性属性。

以下代码示例可防止表格具有包含 firstNamelastName 列的同一组值的两行:

    @Entity(indices = arrayOf(Index(value = ["first_name", "last_name"],
            unique = true)))
    data class User(
        @PrimaryKey val id: Int,
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?,
        @Ignore var picture: Bitmap?
    )

添加基于AutoValue的对象

此功能旨在用于基于 Java 的实体。要在基于 Kotlin 的实体中实现相同的功能,最好改用数据类

在 Room 2.1.0 及更高版本中,您可以将基于 Java 的不可变值类(使用 @AutoValue 为其注释)用作应用的数据库中的实体。此支持在实体的两个实例被视为相等(如果这两个实例的列包含相同的值)时尤为有用。

将带有 @AutoValue 注释的类用作实体时,您可以使用 @PrimaryKey@ColumnInfo@Embedded@Relation 为类的抽象方法注释。不过,您必须在每次使用这些注释时添加 @CopyAnnotations 注释,以便 Room 可以正确解释这些方法的自动生成实现。

以下代码段展示了一个使用 @AutoValue 注释的类(Room 将其标识为实体)的示例:

User.java

    @AutoValue
    @Entity
    public abstract class User {
        // Supported annotations must include `@CopyAnnotations`.
        @CopyAnnotations
        @PrimaryKey
        public abstract long getId();

        public abstract String getFirstName();
        public abstract String getLastName();

        // Room uses this factory method to create User objects.
        public static User create(long id, String firstName, String lastName) {
            return new AutoValue_User(id, firstName, lastName);
        }
    }

定义对象之间的关系

由于 SQLite 是关系型数据库,因此您可以指定各个对象之间的关系。尽管大多数对象关系映射库都允许实体对象互相引用,但 Room 明确禁止这样做

定义一对多关系

即使您不能使用直接关系,Room 仍允许您定义实体之间的外键约束。

例如,如果存在另一个名为 Book 的实体,您可以使用 @ForeignKey 注释定义该实体与 User 实体的关系,如以下代码段所示:

    @Entity(foreignKeys = arrayOf(ForeignKey(
                entity = User::class,
                parentColumns = arrayOf("id"),
                childColumns = arrayOf("user_id"))
           )
    )
    data class Book(
        @PrimaryKey val bookId: Int,
        val title: String?,
        @ColumnInfo(name = "user_id") val userId: Int
    )

由于零个或更多个 Book 实例可以通过 user_id 外键关联到一个 User 实例,因此这会在 UserBook 之间构建一对多关系模型。

外键非常强大,可让您指定引用的实体更新后会发生什么。例如,您可以通过在 @ForeignKey 注释中添加 onDelete = CASCADE,在 User 的对应实例删除后告知 SQLite 删除该用户的所有图书。

注意:SQLite 将 @Insert(onConflict = REPLACE) 作为一组 REMOVEREPLACE 操作(而不是单个 UPDATE 操作)处理。这种替换冲突值的方法可能会影响您的外键约束。如需了解详情,请参阅有关 ON_CONFLICT 子句的 SQLite 文档

创建嵌套对象

有时,您可能希望在数据库逻辑中将某个实体或数据对象表示为一个紧密的整体,即使该对象包含多个字段也是如此。在这些情况下,您可以使用 @Embedded 注释表示要解构到表中其子字段的对象。然后,您可以像查询其他各个列一样查询嵌套字段。

例如,您的 User 类可以包含类型 Address 的字段,该类型表示一组分别名为 streetcitystatepostCode 的字段。要在表中单独存储组成的列,请在 User 类(使用 @Embedded 注释)中添加 Address 字段,如以下代码段所示:

    data class Address(
        val street: String?,
        val state: String?,
        val city: String?,
        @ColumnInfo(name = "post_code") val postCode: Int
    )

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        @Embedded val address: Address?
    )

然后,表示 User 对象的表会包含具有以下名称的列:idfirstNamestreetstatecitypost_code

注意:嵌套字段还可以包含其他嵌套字段。

如果某个实体具有同一类型的多个嵌套字段,您可以通过设置 prefix 属性确保每个列都独一无二。然后,Room 会将提供的值添加到嵌套对象中每个列名称的开头。

	 @Embedded(prefix = "loc_")
   Coordinates coordinates;

定义多对多关系

您通常希望在关系型数据库中构建的另一种关系模型是两个实体之间的多对多关系,其中每个实体都可以关联到另一个实体的零个或更多个实例。

例如,假设有一个音乐在线播放应用,用户可以在该应用中将自己喜爱的歌曲整理到播放列表中。每个播放列表都可以包含任意数量的歌曲,每首歌曲都可以包含在任意数量的播放列表中。

要构建这种关系的模型,您需要创建下面三个对象:

  1. 播放列表的实体类。
  2. 歌曲的实体类。
  3. 用于保存每个播放列表中的歌曲相关信息的中间类。

您可以将实体类定义为独立单元:

    @Entity
    data class Playlist(
        @PrimaryKey var id: Int,
        val name: String?,
        val description: String?
    )

    @Entity
    data class Song(
        @PrimaryKey var id: Int,
        val songName: String?,
        val artistName: String?
    )

然后,将中间类定义为包含对 SongPlaylist 的外键引用的实体:

    @Entity(tableName = "playlist_song_join",
            primaryKeys = arrayOf("playlistId","songId"),
            foreignKeys = arrayOf(
                             ForeignKey(entity = Playlist::class,
                                        parentColumns = arrayOf("id"),
                                        childColumns = arrayOf("playlistId")),
                             ForeignKey(entity = Song::class,
                                        parentColumns = arrayOf("id"),
                                        childColumns = arrayOf("songId"))
                                  )
            )
    data class PlaylistSongJoin(
        val playlistId: Int,
        val songId: Int
    )

这会生成一个多对多关系模型。借助该模型,您可以使用 DAO 按歌曲查询播放列表和按播放列表查询歌曲:

    @Dao
    interface PlaylistSongJoinDao {
        @Insert
        fun insert(playlistSongJoin: PlaylistSongJoin)

        @Query("""
               SELECT * FROM playlist
               INNER JOIN playlist_song_join
               ON playlist.id=playlist_song_join.playlistId
               WHERE playlist_song_join.songId=:songId
               """)
        fun getPlaylistsForSong(songId: Int): Array<Playlist>

        @Query("""
               SELECT * FROM song
               INNER JOIN playlist_song_join
               ON song.id=playlist_song_join.songId
               WHERE playlist_song_join.playlistId=:playlistId
               """)
        fun getSongsForPlaylist(playlistId: Int): Array<Song>
    }

在数据库中创建视图

2.1.0 及更高版本的 Room 持久性库SQLite 数据库视图提供了支持,从而允许您将查询封装到类中。Room 将这些查询支持的类称为视图,在 DAO 中使用时,它们的行为与简单数据对象的行为相同。

注意:与实体类似,您可以针对视图运行 SELECT 语句。不过,您无法针对视图运行 INSERTUPDATEDELETE 语句。

创建视图

要创建视图,请将 @DatabaseView 注释添加到类中。将注释的值设为类应该表示的查询。

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

将视图与数据库相关联

要将此视图添加为应用数据库的一部分,请在应用的 @Database 注释中添加 views 属性:

    @Database(entities = arrayOf(User::class),
              views = arrayOf(UserDetail::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

使用DAO访问数据

要使用 Room 持久性库访问应用的数据,您需要使用数据访问对象 (DAO)。这些 Dao 对象构成了 Room 的主要组件,因为每个 DAO 都包含一些方法,这些方法提供对应用数据库的抽象访问权限。

DAO 既可以是接口,也可以是抽象类。如果是抽象类,则该 DAO 可以选择有一个以 RoomDatabase 为唯一参数的构造函数。Room 会在编译时创建每个 DAO 实现。

注意:除非您对构建器调用 allowMainThreadQueries(),否则 Room 不支持在主线程上访问数据库,因为它可能会长时间锁定界面。异步查询(返回 LiveDataFlowable 实例的查询)无需遵守此规则,因为此类查询会根据需要在后台线程上异步运行查询。

自定义方法

Insert

当您创建 DAO 方法并使用 @Insert 对其进行注释时,Room 会生成一个实现,该实现在单个事务中将所有参数插入到数据库中。

以下代码段展示了几个示例查询:

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

        @Insert
        fun insertBothUsers(user1: User, user2: User)

        @Insert
        fun insertUsersAndFriends(user: User, friends: List<User>)
    }

如果 @Insert 方法只接收 1 个参数,则可返回 long,这是插入项的新 rowId。如果参数是数组或集合,则应返回 long[]List

如需了解详情,请参阅 @Insert 注释的参考文档以及 rowid 表格的 SQLite 文档

Update

Update 便捷方法会修改数据库中以参数形式给出的一组实体。它使用与每个实体的主键匹配的查询。

以下代码段演示了如何定义此方法:

    @Dao
    interface MyDao {
        @Update
        fun updateUsers(vararg users: User)
    }

虽然通常没有必要,但您可以让此方法返回一个 int 值,表示数据库中更新的行数

Delete

Delete 便捷方法会从数据库中删除一组以参数形式给出的实体。它使用主键查找要删除的实体。

以下代码段演示了如何定义此方法:

    @Dao
    interface MyDao {
        @Delete
        fun deleteUsers(vararg users: User)
    }

虽然通常没有必要,但您可以让此方法返回一个 int 值,表示从数据库中删除的行数。

查询信息

@Query 是 DAO 类中使用的主要注释。它允许您对数据库执行读/写操作。每个 @Query 方法都会在编译时进行验证,因此如果查询出现问题,则会发生编译错误,而不是运行时失败。

Room 还会验证查询的返回值,这样的话,当返回的对象中的字段名称与查询响应中的对应列名称不匹配时,Room 会通过以下两种方式之一提醒您:

  • 如果只有部分字段名称匹配,则会发出警告。

  • 如果没有任何字段名称匹配,则会发出错误。

简单查询
    @Dao
    interface MyDao {
        @Query("SELECT * FROM user")
        fun loadAllUsers(): Array<User>
    }

这是一个极其简单的查询,可加载所有用户。在编译时,Room 知道它在查询用户表中的所有列。如果查询包含语法错误,或者数据库中没有用户表格,则 Room 会在您的应用编译时显示包含相应消息的错误。

将参数传递给查询

在大多数情况下,您需要将参数传递给查询以执行过滤操作,例如仅显示某个年龄以上的用户。要完成此任务,请在 Room 注释中使用方法参数,如以下代码段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        fun loadAllUsersOlderThan(minAge: Int): Array<User>
    }

在编译时处理此查询时,Room 会将 :minAge 绑定参数与 minAge 方法参数相匹配。Room 通过参数名称进行匹配。如果有不匹配的情况,则应用编译时会出现错误。

您还可以在查询中传递多个参数或多次引用这些参数,如以下代码段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        fun findUserWithName(search: String): List<User>
    }
返回列的子集

大多数情况下,您只需获取实体的几个字段。例如,您的界面可能仅显示用户的名字和姓氏,而不是用户的每一条详细信息。通过仅提取应用界面中显示的列,您可以节省宝贵的资源,并且您的查询也能更快完成。

借助 Room,您可以从查询中返回任何基于 Java 的对象,前提是结果列集合会映射到返回的对象。例如,您可以创建以下基于 Java 的普通对象 (POJO) 来获取用户的名字和姓氏:

    data class NameTuple(
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )

现在,您可以在查询方法中使用此 POJO:

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        fun loadFullName(): List<NameTuple>
    }

Room 知道该查询会返回 first_namelast_name 列的值,并且这些值会映射到 NameTuple 类的字段。因此,Room 可以生成正确的代码。如果查询返回太多的列,或者返回 NameTuple 类中不存在的列,则 Room 会显示一条警告。

注意:这些 POJO 也可以使用 @Embedded 注释。

传递参数的集合

部分查询可能要求您传入数量不定的参数,参数的确切数量要到运行时才知道。例如,您可能希望从部分区域中检索所有用户的相关信息。Room 知道参数何时表示集合,并根据提供的参数数量在运行时自动将其展开。

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
    }
可观察查询

执行查询时,您通常会希望应用的界面在数据发生变化时自动更新。为此,请在查询方法说明中使用 LiveData 类型的返回值。当数据库更新时,Room 会生成更新 LiveData 所必需的所有代码。

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
    }

注意:自版本 1.0 起,Room 会根据在查询中访问的表格列表决定是否更新 LiveData 实例。

使用RxJava进行响应式查询

Room 为 RxJava2 类型的返回值提供了以下支持:

要使用此功能,请在应用的 build.gradle 文件中添加最新版本的 rxjava2 工件:

app/build.gradle

  dependencies {    
    def room_version = "2.1.0"    
    implementation 'androidx.room:room-rxjava2:$room_version'  
  }

以下代码段展示了几个如何使用这些返回类型的示例:

    @Dao
    interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        fun loadUserById(id: Int): Flowable<User>

        // Emits the number of users added to the database.
        @Insert
        fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

        // Makes sure that the operation finishes successfully.
        @Insert
        fun insertLargeNumberOfUsers(varargs users: User): Completable

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        fun deleteAllUsers(users: List<User>): Single<Int>
    }

参阅 Google Developers Room 和 RxJava 一文

直接光标访问

如果应用的逻辑需要直接访问返回行,您可以从查询返回 Cursor 对象,如以下代码段所示:

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        fun loadRawUsersOlderThan(minAge: Int): Cursor
    }    

注意:强烈建议您不要使用 Cursor API,因为它无法保证行是否存在或者行包含哪些值。只有当您已具有需要光标且无法轻松重构的代码时,才使用此功能。

查询多个表格

部分查询可能需要访问多个表格才能计算出结果。借助 Room,您可以编写任何查询,因此您也可以联接表格。此外,如果响应是可观察数据类型(如 FlowableLiveData),Room 会观察查询中引用的所有表格,以确定是否存在无效表格。

以下代码段展示了如何执行表格联接来整合两个表格的信息:一个表格包含当前借阅图书的用户,另一个表格包含当前处于已被借阅状态的图书的数据。

    @Dao
    interface MyDao {
        @Query(
            "SELECT * FROM book " +
            "INNER JOIN loan ON loan.book_id = book.id " +
            "INNER JOIN user ON user.id = loan.user_id " +
            "WHERE user.name LIKE :userName"
        )
        fun findBooksBorrowedByNameSync(userName: String): List<Book>
    }

您还可以从这些查询中返回 POJO。例如,您可以编写一条加载某位用户及其宠物名字的查询,如下所示:

    @Dao
    interface MyDao {
        @Query(
            "SELECT user.name AS userName, pet.name AS petName " +
            "FROM user, pet " +
            "WHERE user.id = pet.user_id"
        )
        fun loadUserAndPetNames(): LiveData<List<UserPet>>

        // You can also define this class in a separate file.
        data class UserPet(val userName: String?, val petName: String?)
    }    
使用Kotlin协程编写异步方法

suspend Kotlin 关键字添加到 DAO 方法,以使用 Kotlin 协程功能使这些方法成为异步方法。这样可确保不会在主线程上执行这些方法。

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

        @Update
        suspend fun updateUsers(vararg users: User)

        @Delete
        suspend fun deleteUsers(vararg users: User)

        @Query("SELECT * FROM user")
        suspend fun loadAllUsers(): Array<User>
    }

注意:要将 Room 与 Kotlin 协程一起使用,您需要使用 Room 2.1.0、Kotlin 1.3.0 和 Cordoines 1.0.0 或更高版本。如需了解详情,请参阅声明依赖项

本指南也适用于带有 @Transaction 注释的 DAO 方法。您可以使用此功能通过其他 DAO 方法构建暂停数据库方法。然后,这些方法会在单个数据库事务中运行。

@Dao
    abstract class UsersDao {
        @Transaction
        open suspend fun setLoggedInUser(loggedInUser: User) {
            deleteUser(loggedInUser)
            insertUser(loggedInUser)
        }

        @Query("DELETE FROM users")
        abstract fun deleteUser(user: User)

        @Insert
        abstract suspend fun insertUser(user: User)
    }

注意:应避免在单个数据库事务中执行额外的应用端工作,因为 Room 会将此类事务视为独占事务,并且按顺序每次仅执行一个事务。也就是说,包含不必要操作的事务很容易锁定您的数据库并影响性能。

预填充数据库

迁移数据库

测试和调试数据库

引用复杂数据

使用类型转换器

了解Room为何不允许对象引用

要点:Room 不允许实体类之间进行对象引用。因此,您必须明确请求您的应用所需的数据。

映射从数据库到相应对象模型之间的关系是一种常见做法,极其适用于服务器端。即使程序在访问字段时加载字段,服务器仍然可以正常工作。

但在客户端,这种延迟加载是不可行的,因为它通常发生在界面线程上,并且在界面线程上查询磁盘上的信息会导致严重的性能问题。界面线程通常需要大约 16 毫秒来计算和绘制 Activity 的更新后的布局,因此,即使查询只用了 5 毫秒,您的应用仍然可能会用尽剩余的时间来绘制框架,从而导致明显的显示故障。如果有一个并行运行的单独事务,或者设备正在运行其他磁盘密集型任务,则查询可能需要更多时间才能完成。不过,如果您不使用延迟加载,则应用会抓取一些不必要的数据,从而导致内存消耗问题。

对象关系型映射通常将决定权留给开发者,以便他们可以针对自己的应用用例执行最合适的操作。开发者通常会决定在应用和界面之间共享模型。不过,这种解决方案并不能很好地扩展,因为界面会不断发生变化,共享模型会出现开发者难以预测和调试的问题。

例如,假设界面加载了 Book 对象的列表,其中每本图书都有一个 Author 对象。您最初可能设计让查询使用延迟加载,从而让 Book 实例检索作者。对 author 字段的第一次检索会查询数据库。一段时间后,您发现还需要在应用的界面中显示作者姓名。您可以轻松访问此名称,如以下代码段所示:

authorNameTextView.text = book.author.name

不过,这种看似无害的更改会导致在主线程上查询 Author 表。

如果您事先查询作者信息,则在您不再需要这些数据时,就会很难更改数据加载方式。例如,如果应用的界面不再需要显示 Author 信息,则应用会有效地加载不再显示的数据,从而浪费宝贵的内存空间。如果 Author 类引用其他表(例如 Books),则应用的效率会进一步下降。

要使用 Room 同时引用多个实体,请改为创建包含每个实体的 POJO,然后编写用于联接相应表的查询。这种结构合理的模型结合 Room 强大的查询验证功能,可让您的应用在加载数据时消耗较少的资源,从而改善应用的性能和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值