Android Jetpack-Room数据库的使用

7,310 阅读14分钟

Adroid Jetpack Room数据库的使用

首先还是先添加maven依赖:

dependencies {
  def room_version = "2.2.0-alpha01"

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

使用Room保存数据到本地数据库中

Room在SQlite上提供了一层抽象层,允许你流畅的访问SQLite数据库的全部功能。

当应用程序在处理大量的数据结构的时候,能从本地数据获得很大的帮助。最常见的用例上缓存相关的数据,这样,当设备不能访问网络,用户仍然能离线浏览内容。然后在设备重新联网后,任何用户发起的内容修改都能同步到服务器。

由于Room会为我们解决这些问题,因此Google强烈推荐我们使用Room而不是SQLite。

Room主要由3个部分组成:

  • Database: 包含数据库持有者,并作为应用程序持久关系数据的基础连接的主要访问点

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

  • 是一个继承于RoomDatabase的抽象类
  • 在注解中包括与数据库相关联的实体列表
  • 包含一个没有参数的抽象方法并且返回一个带有注解的 @Dao

在运行时,我们可以通过调用 Room.databaseBuilder()Room.inMemoryDatabaseBuilder() 来获取 Database 的实例。

  • Entity: 表示数据库中的表
  • DAO: 包含用于访问数据库的方法

这些组件以及它们与应用程序其余部分的关系如图1所示:

room_architecture

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

User.kt

@Entity(tableName = "users")
data class User(
    @PrimaryKey
    @ColumnInfo(name = "userid") val id: String = UUID.randomUUID().toString(),
    @ColumnInfo(name = "username") val userName: String)

UserDao.kt

@Dao
interface UserDao {

    @Query("SELECT * FROM Users WHERE userid = :id")
    fun getUserById(id: String): User?
    
    /*当数据库中已经有此用户的时候,直接替换*/
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User)

    @Update
    fun upDateUser(user: User)

    @Query("DELETE FROM Users")
    fun deleteAllUsers()
}

AppDatabase.kt

@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase = INSTANCE ?: synchronized(this) {
            INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
        }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(
                context.applicationContext,
                UsersDatabase::class.java, "Sample.db")
                .build()
    }
}

⚠️注意:数据库的实例化是很昂贵的,所以建议我们使用单例模式来初始化,而且也很少情况下需要访问多个实例。

使用Room实体定义数据

​ 默认情况下,Room会为实体类中定义的每个字段创建一列,如果实体中有我们不想保留的字段,可以使用@Ignore 注解它们。我们必须通过 Database 类中的 entities 数组来引用实体类。

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

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

要想存储一个字段,Room必须能够访问到它。我们可以将字段设为public,也可以提供getter和setter方法,如果是使用的getter和setter方法的话,那么它们要基于Room中的JavaBeans的约定。

⚠️注意:实体可以有一个空的构造函数(如果相应的DAO类可以访问每个持久化字段)或构造函数的参数包含匹配字段的类型和名称的实体。Room还可以使用全部或部分的构造函数,如构造函数只收到的一些字段。

使用主键

每个实体类都必须至少定义一个主键。即使只有一个字段,我们仍然需要使用@PrimaryKey注解。另外,如果想要使用Room自动分配实体ID,我们可以设置@PrimaryKeyautoGenerate 属性,如果实体具有复合主键,则可以使用注解 @EntityprimaryKeys属性,如以下代码所示:

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

默认情况下,Room使用实体类名作为数据库表名。如果你想设置不同的表名称,使用@Entity的属性tableName来进行注解,如下面的代码片段所示:

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

⚠️注意:表名在数据库是不区分大小写的

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?)
Ignore注解

​ 默认情况下,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()

定义对象之间的关系

​ 因为SQLite是一个关系数据库,你可以定义2个对象之间的关系。尽管大多数对象关系映射库允许实体对象相互引用,但Room 明确禁止这样做,要想了解此决策背后的技术原理,请看 Understand why Room doesn't allow object references

定义一对多个关系

即使你不能使用直接关系,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
)

外键非常强大,因为它允许我们指定:引用的实体类更新时发生的情况。例如,如果要告诉SQLite删除 user 的所有books,可以通过在注解 @ForeignKey 中包含 onDelete = CASCADE 来删除相应的User实例。

⚠️注意:SQLite处理 @Insert(onConflict = REPLACE) 作为一组REMOVEREPLACE 操作,而不是单个的 UPDATE 操作。这种替换冲突值的方法可能会影响外键约束。

创建嵌套对象

有时,我们希望将实体或普通旧Java对象(POJO)表示为数据库逻辑中的一个组合体,也就是让该对象包含多个字段。在这些情况下,我们可以使用注解 @Embedded 来表示要分解到表中子字段的对象。然后我们就可以像查找其他单个列一样查询嵌入字段。

例如,用户类可以包含一个字段类型的地址,代表组成的字段命名的街道,城市,国家,邮编。存储由单独列在表中,包括一个地址字段与@Embedded用户注释的类,如下面代码片段所示:

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?
)

⚠️注意:嵌入字段还可以嵌入其他字段

定义多对多的关系

有另一种关系,你经常想在关系数据库模型:两个实体之间的多对多关系,每个实体可以与其他的零个或多个实例。例如,考虑一个音乐流媒体应用,用户可以将自己喜欢的歌曲到播放列表。每一个播放列表可以拥有任意数量的歌,每首歌可以包含任意数量的播放列表

这种关系模型,您将需要创建三个对象:

​ 播放列表的一个实体类。

  歌曲的一个实体类。   

​ 一个调度类控制信息哪一首歌曲在哪一个列表里面。

你可以单独的定义实体类

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

然后,中间类定义为一个实体包含外键引用歌曲和播放列表

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

使用Room DAO访问数据

要使用 Room 数据库访问应用程序的数据 ,我们需要使用数据访问对象 或 DAO。这组 Dao 对象是构成Room的主要组件,因为每个 DAO 都包含抽象访问我们应用程序数据库的方法。

⚠️注意:Room不支持主线程上的数据库访问,因为它可能会长时间锁定 UI, 除非我们调用 RoomDatabase.Builder 的 allowMainThreadQueries() 。

异步查询 - 返回 LiveData or Flowable 实例的查询不受此规则影响,因为它们在需要时会在后台线程上运行异步查询。

DAO 可以是接口,也可以是抽象类。如果它是一个抽象类,它可以选择使用一个构造函数,该构造函数将 RoomDatabase 作为唯一参数。Room在编译期创建每个DAO 的实现

定义便利的方法
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<Long> 替代。

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

@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>
}
返回列的子集

大多数时候,你只需要一个实体里面的几个字段。例如,UI 可能只显示用户的名字和姓氏,而不是每个用户的详细信息。通过仅获取应用程序 UI 中显示的列,可以节省宝贵的资源,并且可以更快地完成查询。

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>
}
可观察的查询

执行查询时,我们通常希望应用程序的 UI 在数据更改时自动更新。要实现此目的,可以在查询方法描述中使用 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使用的表访问的tables查询决定是否更新LiveData的实例

使用RxJava进行响应式查询

Room 还可以从我们定义的查询中返回 RxJava2 的 Publisher 和 Flowable 对象。

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

要使用这个功能,添加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>
}

更多细节,请参见谷歌开发者的 Room and RxJava文章。

游标卡尺查询

如果你的应用程序的逻辑需要直接访问返回行,您可以从您的查询返回一个游标对象,如下面代码片段所示:

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

⚠️注意:Google 非常不鼓励使用 Cursor API,因为它不保证行是否存在或 行 包含的值。仅当我们已经拥有需要光标的代码且无法轻松重构时才使用此功能。

查询多个表

某些查询可能需要访问多个表来计算结果。Room允许编写任何查询,因此我们也可以连接表。此外,如果响应是可观察的数据类型,例如 LiveDataFlowable ,Room 则会监视查询中引用的所有失效的表。

下面的代码片段展示了如何执行表连接信息表包含用户借阅书籍和一个表包含数据书目前租借

@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的协程编写异步方法

你可以通过添加kotlin的关键字suspend到你的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协助需要Room2.1.0及以上,kotlin1.3.0版本,协程1.0.0或更高。有关更多信息,请参见Declaring dependencies.

还有迁移数据库和测试数据库本文不作诠释了,如需要了解请访问迁移数据库和[测试数据库](