Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
Room 包含 3 个主要组件:
数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。
使用 @Database
注释的类应满足以下条件:
RoomDatabase
的抽象类。@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,则可以设置 @PrimaryKey
的 autoGenerate
属性。如果实体具有复合主键,您可以使用 @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) 快速访问数据库信息,请使用虚拟表(使用 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
来强制实施此唯一性属性。
以下代码示例可防止表格具有包含 firstName
和 lastName
列的同一组值的两行:
@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?
)
此功能旨在用于基于 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
实例,因此这会在 User
和 Book
之间构建一对多关系模型。
外键非常强大,可让您指定引用的实体更新后会发生什么。例如,您可以通过在 @ForeignKey
注释中添加 onDelete = CASCADE
,在 User
的对应实例删除后告知 SQLite 删除该用户的所有图书。
注意:SQLite 将
@Insert(onConflict = REPLACE)
作为一组REMOVE
和REPLACE
操作(而不是单个UPDATE
操作)处理。这种替换冲突值的方法可能会影响您的外键约束。如需了解详情,请参阅有关ON_CONFLICT
子句的 SQLite 文档。
有时,您可能希望在数据库逻辑中将某个实体或数据对象表示为一个紧密的整体,即使该对象包含多个字段也是如此。在这些情况下,您可以使用 @Embedded
注释表示要解构到表中其子字段的对象。然后,您可以像查询其他各个列一样查询嵌套字段。
例如,您的 User
类可以包含类型 Address
的字段,该类型表示一组分别名为 street
、city
、state
和 postCode
的字段。要在表中单独存储组成的列,请在 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
对象的表会包含具有以下名称的列:id
、firstName
、street
、state
、city
和 post_code
。
注意:嵌套字段还可以包含其他嵌套字段。
如果某个实体具有同一类型的多个嵌套字段,您可以通过设置 prefix
属性确保每个列都独一无二。然后,Room 会将提供的值添加到嵌套对象中每个列名称的开头。
@Embedded(prefix = "loc_")
Coordinates coordinates;
您通常希望在关系型数据库中构建的另一种关系模型是两个实体之间的多对多关系,其中每个实体都可以关联到另一个实体的零个或更多个实例。
例如,假设有一个音乐在线播放应用,用户可以在该应用中将自己喜爱的歌曲整理到播放列表中。每个播放列表都可以包含任意数量的歌曲,每首歌曲都可以包含在任意数量的播放列表中。
要构建这种关系的模型,您需要创建下面三个对象:
您可以将实体类定义为独立单元:
@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?
)
然后,将中间类定义为包含对 Song
和 Playlist
的外键引用的实体:
@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
语句。不过,您无法针对视图运行INSERT
、UPDATE
或DELETE
语句。
要创建视图,请将 @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
}
要使用 Room 持久性库访问应用的数据,您需要使用数据访问对象 (DAO)。这些
Dao
对象构成了 Room 的主要组件,因为每个 DAO 都包含一些方法,这些方法提供对应用数据库的抽象访问权限。
DAO 既可以是接口,也可以是抽象类。如果是抽象类,则该 DAO 可以选择有一个以
RoomDatabase
为唯一参数的构造函数。Room 会在编译时创建每个 DAO 实现。注意:除非您对构建器调用
allowMainThreadQueries()
,否则 Room 不支持在主线程上访问数据库,因为它可能会长时间锁定界面。异步查询(返回LiveData
或Flowable
实例的查询)无需遵守此规则,因为此类查询会根据需要在后台线程上异步运行查询。
当您创建 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
便捷方法会修改数据库中以参数形式给出的一组实体。它使用与每个实体的主键匹配的查询。
以下代码段演示了如何定义此方法:
@Dao
interface MyDao {
@Update
fun updateUsers(vararg users: User)
}
虽然通常没有必要,但您可以让此方法返回一个
int
值,表示数据库中更新的行数
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_name
和 last_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
实例。
Room 为 RxJava2 类型的返回值提供了以下支持:
@Query
方法:Room 支持 Publisher
、Flowable
和 Observable
类型的返回值。@Insert
、@Update
和 @Delete
方法:Room 2.1.0 及更高版本支持 Completable
、Single
和 Maybe
类型的返回值。要使用此功能,请在应用的 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,您可以编写任何查询,因此您也可以联接表格。此外,如果响应是可观察数据类型(如 Flowable
或 LiveData
),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?)
}
将 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 不允许实体类之间进行对象引用。因此,您必须明确请求您的应用所需的数据。
映射从数据库到相应对象模型之间的关系是一种常见做法,极其适用于服务器端。即使程序在访问字段时加载字段,服务器仍然可以正常工作。
但在客户端,这种延迟加载是不可行的,因为它通常发生在界面线程上,并且在界面线程上查询磁盘上的信息会导致严重的性能问题。界面线程通常需要大约 16 毫秒来计算和绘制 Activity 的更新后的布局,因此,即使查询只用了 5 毫秒,您的应用仍然可能会用尽剩余的时间来绘制框架,从而导致明显的显示故障。如果有一个并行运行的单独事务,或者设备正在运行其他磁盘密集型任务,则查询可能需要更多时间才能完成。不过,如果您不使用延迟加载,则应用会抓取一些不必要的数据,从而导致内存消耗问题。
对象关系型映射通常将决定权留给开发者,以便他们可以针对自己的应用用例执行最合适的操作。开发者通常会决定在应用和界面之间共享模型。不过,这种解决方案并不能很好地扩展,因为界面会不断发生变化,共享模型会出现开发者难以预测和调试的问题。
例如,假设界面加载了
Book
对象的列表,其中每本图书都有一个Author
对象。您最初可能设计让查询使用延迟加载,从而让Book
实例检索作者。对author
字段的第一次检索会查询数据库。一段时间后,您发现还需要在应用的界面中显示作者姓名。您可以轻松访问此名称,如以下代码段所示:
authorNameTextView.text = book.author.name
不过,这种看似无害的更改会导致在主线程上查询
Author
表。如果您事先查询作者信息,则在您不再需要这些数据时,就会很难更改数据加载方式。例如,如果应用的界面不再需要显示
Author
信息,则应用会有效地加载不再显示的数据,从而浪费宝贵的内存空间。如果Author
类引用其他表(例如Books
),则应用的效率会进一步下降。要使用 Room 同时引用多个实体,请改为创建包含每个实体的 POJO,然后编写用于联接相应表的查询。这种结构合理的模型结合 Room 强大的查询验证功能,可让您的应用在加载数据时消耗较少的资源,从而改善应用的性能和用户体验。