Android架构师必学——Jetpack Room数据库应用与源码解析

语言: CN / TW / HK

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第24天,点击查看活动详情

Room是Google官方在SQLite基础上封装的一款数据持久库,是Jetpack全家桶的一员,和Jetpack其他库有着可以高度搭配协调的天然优势。Room使用APT技术,大大简化了使用SQLite的代码量,只需使用注解配合少量代码即可实现高效的数据库操作。

Room介绍

  1. Room是一个OM(Object Mapping对象映射)数据库,可以方便地在Android应用程序上访问数据库。
  2. Room抽象了SQLite,通过提供方便的api来查询数据库,并在编译时验证。并且可以使用SQLite的全部功能,同时拥有Java SQL查询生成器提供的类型安全。

Room的构成

  1. Database:数据库扩展了RoomDatabase的抽象类。可以通过Room获得它的一个实例。databaseBuilder或Room.inMemoryDatabaseBuilder。
  2. Entity:代表一个表结构。
  3. Dao:数据访问对象是Room的主要组件,负责定义访问数据库的方法。

优点:

  • Google官方库,和Jetpack其他库(比如Lifecycle,LiveData)等有天然的融合搭配使用优势。
  • 在编译器可以对SQL语法进行检查。
  • 使用APT,简单地几个注解搭配少量代码即可使用,大量减少模板代码。
  • 查询代码自定义,可以实现更复杂的查询功能,也可以对SQL语句进行优化。
  • 简化数据库的迁移路径。

不足:

  • 查询时必须手动写SQL语句,不提供默认的查询配置。
  • 效率比其他数据库框架(GreenDao等)并没有多少提高。
  • 数据库版本升级稍显复杂。

一、基本介绍

框架特点

相对于SQLiteOpenHelper等传统方法,使用Room操作SQLite有以下优势:

  • 编译期的SQL语法检查
  • 开发高效,避免大量模板代码
  • API设计友好,容易理解
  • 可以与RxJavaLiveDataKotlin Coroutines等进行桥接

添加依赖

dependencies { implementation "androidx.room:room-runtime:2.2.5" kapt "androidx.room:room-compiler:2.2.5" }

基本组件

Room的使用,主要涉及以下3个组件

  • Database: 访问底层数据库的入口
  • Entity: 代表数据库中的表(table),一般用注解
  • Data Access Object (DAO): 数据库访问者

这三个组件的概念也出现在其他ORM框架中,有过使用经验的同学理解起来并不困难: 通过Database获取DAO,然后通过DAO查询并获取entities,最终通过entities对数据库table中数据进行读写

在这里插入图片描述

Database

Database是我们访问底层数据库的入口,管理着真正的数据库文件。我们使用@Database定义一个Database类:

  • 派生自RoomDatabase
  • 关联其内部数据库table对应的entities
  • 提供获取DAO的抽象方法,且不能有参数

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

运行时,我们可以通过Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()获取Database实例

val db = Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).build()

创建Databsse的成本较高,推荐使用单例的Database,避免反复创建实例带来的开销

Entity

一个Entity代表数据库中的一张表(table)。我们使用@Entity定义一个Entiry类,类中的属性对应表中的Column

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

  • 所有的属性必须是public、或者有get、set方法
  • 属性中至少有一个主键,使用@PrimaryKey表示单个主键,也可以像下面这样定义多主键

@Entity(primaryKeys = arrayOf("firstName", "lastName"))

  • 当主键值为null时,autoGenerate可以帮助自动生成键值

@PrimaryKey(autoGenerate = true) val uid : Int

  • 默认情况下使用类名作为数据库table名,也可使用tableName指定

@Entity(tableName = "users")

  • Entity中的所有属性都会被持久化到数据库,除非使用@Ignore

@Ignore val picture: Bitmap?

  • 可以使用indices指定数据库索引,unique设置其为唯一索引

``` @Entity(indices = arrayOf(Index(value = ["last_name", "address"])))

@Entity(indices = arrayOf(Index(value = ["first_name", "last_name"], unique = true))) ```

Data Access Object (DAO)

DAO提供了访问DB的API,我们使用@Dao定义DAO类,使用@Query@Insert@Delete定义CRUD方法

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

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

@Insert fun insertAll(vararg users: User)

@Delete fun delete(user: User) } ```

DAO的方法调用都在当前线程进行,所以要避免在UI线程直接访问

Type Converters

有时,需要将自定义类型的数据持久化到DB,此时需要借助Converters进行转换

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

@TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time?.toLong() } } ```

在声明Database时,指定此Converters

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

2. Data Access Objects(DAO)

Room中使用Data Access Objects(DAO)对数据库进行读写,相对于SQL语句直接查询,DAO可以定义更加友好的API。DAO中可以自定义CURD方法,还可以方便地与RxJavaLiveData等进行集成。

我们可以使用接口或者抽象类定一个DAO,如果使用抽象类,可以选择性的为其定义构造函数,并接受Database作为唯一参数。

Room在编译期会基于定义的DAO生成具体实现类,实现具体CURD方法。

@Insert 插入

@Insert注解插入操作,编译期生成的代码会将所有的参数以单独的事务更新到DB。

@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUsers(vararg users: User) @Insert fun insertBothUsers(user1: User, user2: User) @Insert fun insertUsersAndFriends(user: User, friends: List<User>) }

onConflict设置当事务中遇到冲突时的策略

  • OnConflictStrategy.REPLACE : 替换旧值,继续当前事务
  • OnConflictStrategy.ROLLBACK : 回滚当前事务
  • OnConflictStrategy.ABORT : 结束当前事务、回滚
  • OnConflictStrategy.FAIL : 当前事务失败、回滚
  • OnConflictStrategy.NONE : 忽略冲突,继续当前事务

最新代码中ROLLBACK 和 FAIL 已经deprecated了,使用ABORT替代

@Update 更新

@Update注解定义更新操作,根据参数对象的主键更新指定row的数据

@Dao interface UserDao { @Update(onConflict = OnConflictStrategy.REPLACE) fun updateUsers(vararg users: User) @Update fun update(user: User) }

@Delete 删除

@Delete定义删除操作,根据主键删除指定row

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

@Query 查询

@Query注解定义查询操作。@Query中的SQL语句以及返回值类型等会在编译期进行检查,更早的暴露问题

@Dao interface UserDao { @Query("SELECT * FROM users") fun loadAllUsers(): Array<User> }

指定参数

可以用参数指定@Query中的where条件:

``` @Dao interface UserDao { @Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge") fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array

@Query("SELECT * FROM users WHERE first_name LIKE :search " +
       "OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>

} ```

返回子集

返回的结果可以是所有column的子集:

data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? )@Dao interface UserDao { @Query("SELECT first_name, last_name FROM users") fun loadFullName(): List<NameTuple> }

返回Cursor

返回Cursor,可以基于Cursor进行进一步操作

@Dao interface UserDao { @Query("SELECT * FROM users") fun loadAllUsers(): Cursor }

多表查询

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

SQL可以写任何语句,包括多表连接等

返回类型

Room可以返回Coroutine、RxJava等多个常用库的类型结果,便于在异步、响应式开发中使用 在这里插入图片描述

3.实体与数据表关系

对于关系型数据库来说,最重要的是如何将数据拆分为有相关关系的多个数据表。SQLite作为关系型数据库,允许entits之间可以有多种关系,Room提供了多种方式表达这种关系。

@Embedded内嵌对象

@Embedded注解可以将一个Entity作为属性内嵌到另一Entity,我们可以像访问Column一样访问内嵌Entity

内嵌实体本身也可以包括其他内嵌对象

``` data class Address( val street: String?, val state: String?, val city: String?, val postCode: Int )

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

如上,等价于User表包含了 id, firstName, street, state, city, postCode等column

如果内嵌对象中存在同名字段,可以使用prefix指定前缀加以区分

@Embedded通过把内嵌对象的属性解包到被宿主中,建立了实体的连接。此外还可以通过@Relationforeignkeys来描述实体之间更加复杂的关系。

我们至少可以描述三种实体关系

  • 一对一
  • 一对多或多对一
  • 多对多

一对一

主表(Parent Entity)中的每条记录与从表(Child Entity)中的每条记录一一对应。

设想一个音乐app的场景,用户(User)和曲库(Library)有如下关系:

  • 一个User只有一个Library
  • 一个Library只属于唯一User

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

@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "userId", childColumns = "userOwnerId", onDelete = CASCADE)) data class Library( @PrimaryKey val libraryId: Long, val title: String, val userOwnerId: Long )

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

如上,User和Library之间属于一对一的关系。

foreignkeys

foreignkeys作为@Relation的属性用来定义外键约束。外键只能在从表上,从表需要有字段对应到主表的主键(Library的userOwnerId对应到User的userId)。

外键约束属性:当有删除或者更新操作的时候发出这个约束

通过外键约束,对主表的操作会受到从表的影响。例如当在主表(即外键的来源表)中删除对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除。

@Relation

为了能够对User以及关联的Library进行查询,需要为两者之间建立一对一关系:

  • 通过UserAndLibrary定义这种关系,包含两个成员分别是主表和从表的实体
  • 为从表添加@Relation注解
  • parentColumn:主表主键
  • entityColumn:从表外键约束的字段

然后,可以通过UserAndLibrary进行查询

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

此方法要从两个表中分别进行两次查询,所以@Transaction确保方法中的多次查询的原子性

一对多

主表中的一条记录对应从表中的零到多条记录。

在前面音乐APP的例子中,有如下一对多关系:

  • 一个User可以创建多个播放列表(Playlist)
  • 每个Playlist只能有唯一的创作者(User)

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

@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "userId", childColumns = "userCreatorId", onDelete = CASCADE)) data class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String )

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

可以看到,一对多关系的UserWithPlaylists与一对一类似, 只是playlists需要是一个List表示从表中的记录不止一个。

查询方法如下:

@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>

多对多

主表中的一条记录对应从表中的零活多个,反之亦然

  • 每个Playlist中可以有很多首歌曲(Song)
  • 每个Song可以归属不同的Playlist

因此,Playlist与Song之间是多对多的关系

``` @Entity data class Playlist( @PrimaryKey val id: Long, val playlistName: String )

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

@Entity(primaryKeys = ["playlistId", "songId"], foreignKeys = { @ForeignKey(entity = Playlist.class, parentColumns = "id", childColumns = "playlistId"), @ForeignKey(entity = Song.class, parentColumns = "id", childColumns = "songId") }))

data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long ) ```

多对多关系中,Song和Playlist之间没有明确的外键约束关系,需要定义一个 associative entity(又或者称作交叉连接表):PlaylistSongCrossRef,然后分别与Song和Playlist建立外键约束。交叉连接的结果是Song与Playlist的笛卡尔积,即两个表中所有记录的组合。

基于交叉连接表,我们可以获取一首Song与其包含它的所有Playlist,又或者一个Playlist与其包含的所有Song。

如果使用SQL获取指定Playlist与其包含的Song,需要两条查询:

```

查询playlist信息

SELECT * FROM Playlist

查询Song信息

SELECT Song.id AS songId, Song.name AS songName, _junction.playlistId FROM PlaylistSongCrossRef AS _junction INNER JOIN Song ON (_junction.songId = Song.id)

WHERE _junction.playlistId IN (playlistId1, playlistId2, …)

```

如果使用Room,则需要定义PlaylistWithSongs类,并告诉其使用PlaylistSongCrossRef作为连接:

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

同理,也可定义SongWithPlaylists

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

查询与前面类似,很简单:

``` @Transaction @Query("SELECT * FROM Playlist") fun getPlaylistsWithSongs(): List

@Transaction @Query("SELECT * FROM Song") fun getSongsWithPlaylists(): List ```

二、Room使用

Room 的使用可以分为三步:

创建 Entity 类:也就是实体类,每个实体类都会生成一个对应的表,每个字段都会生成对应的一列。

创建 Dao 类:Dao 是指 Data Access Object,即数据访问对象,通常我们会在这里封装对数据库的增删改查操作,这样的话,逻辑层就不需要和数据库打交道了,只需要使用 Dao 类即可。

创建 Database 类:定义数据库的版本,数据库中包含的表、包含的 Dao 类,以及数据库升级逻辑。

创建 Entity 类

新建一个 User 类,并添加 @Entity 注解,使 Room 为此类自动创建一个表。在主键上添加 @PrimaryKey(autoGenerate = true) 注解,使得 id 自增,不妨将这里的主键 id 记作固定写法。

`@Entity``data class User(var firstName: String, var lastName: String, var age: Int) {`` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

创建 Dao 类

创建一个接口类 UserDao,并在此类上添加 @Dao 注解。增删改查方法分别添加 @Insert@Delete@Update@Query 注解,其中,@Query 需要编写 SQL 语句才能实现查询。Room 会自动为我们生成这些数据库操作方法。

`@Dao``interface UserDao {`` ``@Insert`` ``fun insertUser(user: User): Long`` ``@Update`` ``fun updateUser(newUser: User)`` ``@Query("select * from user")`` ``fun loadAllUsers(): List`` ``@Query("select * from User where age > :age")`` ``fun loadUsersOlderThan(age: Int): List`` ``@Delete`` ``fun deleteUser(user: User)`` ``@Query("delete from User where lastName = :lastName")`` ``fun deleteUserByLastName(lastName: String): Int``}`

@Query 方法不仅限于查找,还可以编写我们自定义的 SQL 语句,所以可以用它来执行特殊的 SQL 操作,如上例中的 deleteUserByLastName 方法所示。

创建 Database 抽象类

新建 AppDatabase 类,继承自 RoomDatabase 类,添加 @Database 注解,在其中声明版本号,包含的实体类。并在抽象类中声明获取 Dao 类的抽象方法。

`@Database(version = 1, entities = [User::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

在 getDatabase 方法中,第一个参数一定要使用 applicationContext,以防止内存泄漏,第三个参数表示数据库的名字。

测试

布局中只有四个 id 为 btnAdd,btnDelete,btnUpdate,btnQuery 的按钮,故不再给出布局代码。

MainActivity 代码如下:

`class MainActivity : AppCompatActivity() {`` ``override fun onCreate(savedInstanceState: Bundle?) {`` ``super.onCreate(savedInstanceState)`` ``setContentView(R.layout.activity_main)`` ``val userDao = AppDatabase.getDatabase(this).userDao()`` ``val teacher = User("lin", "guo", 66)`` ``val student = User("alpinist", "wang", 3)`` ``btnAdd.setOnClickListener {`` ``thread {`` ``teacher.id = userDao.insertUser(teacher)`` ``student.id = userDao.insertUser(student)`` ``}`` ``}`` ``btnDelete.setOnClickListener {`` ``thread {`` ``userDao.deleteUser(student)`` ``}`` ``}`` ``btnUpdate.setOnClickListener {`` ``thread {`` ``teacher.age = 666`` ``userDao.updateUser(teacher)`` ``}`` ``}`` ``btnQuery.setOnClickListener {`` ``thread {`` ``Log.d("~~~", "${userDao.loadAllUsers()}")`` ``}`` ``}`` ``}``}`

每一步操作我们都开启了一个新线程来操作,这是由于数据库操作涉及到 IO,所以不推荐在主线程执行。在开发环境中,我们也可以通过 allowMainThreadQueries() 方法允许主线程操作数据库,但一定不要在正式环境使用此方法。

`Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.allowMainThreadQueries()`` ``.build()`

点击 btnAdd,再点击 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=66), User(firstName=alpinist, lastName=wang, age=3)]`

点击 btnDelete,再点击 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=66)]`

点击 btnUpdate,再点击 btnQuery,Log 如下:

`~~~: [User(firstName=lin, lastName=guo, age=666)]`

由此可见,我们的增删改查操作都成功了。

数据库升级

简单升级

使用 fallbackToDestructiveMigration() 可以简单粗暴的升级,也就是直接丢弃旧版本数据库,然后创建最新的数据库

`Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.fallbackToDestructiveMigration()`` ``.build()`

注:此方法过于暴力,开发阶段可使用,不可在正式环境中使用,因为会导致旧版本数据库丢失。

规范升级

新增一张表

创建 Entity 类

`@Entity``data class Book(var name: String, var pages: Int) {`` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

创建 Dao 类

`@Dao``interface BookDao {`` ``@Insert`` ``fun insertBook(book: Book)`` ``@Query("select * from Book")`` ``fun loadAllBooks(): List``}`

修改 Database 类:

`@Database(version = 2, entities = [User::class, Book::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``abstract fun bookDao(): BookDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``private val MIGRATION_1_2 = object : Migration(1, 2) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``create table Book (`` ``id integer primary key autoincrement not null,`` ``name text not null,`` ``pages integer not null)`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.addMigrations(MIGRATION_1_2)`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

注:这里的修改有:

  • version 升级
  • 将 Book 类添加到 entities 中
  • 新增抽象方法 bookDao
  • 创建 Migration 对象,并将其添加到 getDatabase 的 builder 中

现在如果再操作数据库,就会新增一张 Book 表了。

修改一张表

比如在 Book 中新增 author 字段

`@Entity``data class Book(var name: String, var pages: Int, var author: String) {` ` ``@PrimaryKey(autoGenerate = true)`` ``var id: Long = 0``}`

修改 Database,增加版本 2 到 3 的迁移逻辑:

`@Database(version = 3, entities = [User::class, Book::class])``abstract class AppDatabase : RoomDatabase() {`` ``abstract fun userDao(): UserDao`` ``abstract fun bookDao(): BookDao`` ``companion object {`` ``private var instance: AppDatabase? = null`` ``private val MIGRATION_1_2 = object : Migration(1, 2) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``create table Book (`` ``id integer primary key autoincrement not null,`` ``name text not null,`` ``pages integer not null)`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``private val MIGRATION_2_3 = object : Migration(2, 3) {`` ``override fun migrate(database: SupportSQLiteDatabase) {`` ``database.execSQL(`` ``"""`` ``alter table Book add column author text not null default "unknown"`` ``""".trimIndent()`` ``)`` ``}`` ``}`` ``@Synchronized`` ``fun getDatabase(context: Context): AppDatabase {`` ``return instance?.let { it }`` ``?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")`` ``.addMigrations(MIGRATION_1_2, MIGRATION_2_3)`` ``.build()`` ``.apply { instance = this }`` ``}`` ``}``}`

注:这里的修改有:

version 升级创建 Migration 对象,并将其添加到 getDatabase 的 builder 中

测试

修改 MainActivity:

`class MainActivity : AppCompatActivity() {`` ``override fun onCreate(savedInstanceState: Bundle?) {`` ``super.onCreate(savedInstanceState)`` ``setContentView(R.layout.activity_main)`` ``val bookDao = AppDatabase.getDatabase(this).bookDao()`` ``btnAdd.setOnClickListener {`` ``thread {`` ``bookDao.insertBook(Book("第一行代码", 666, "guolin"))`` ``}`` ``}`` ``btnQuery.setOnClickListener {`` ``thread {`` ``Log.d("~~~", "${bookDao.loadAllBooks()}")`` ``}`` ``}`` ``}``}`

点击 btnAdd,再点击 btnQuery,Log 如下:

`~~~: [Book(name=第一行代码, pages=666, author=guolin)]`

这就说明我们对数据库的两次升级都成功了。

三、源码分析

Room在编译期通过kapt处理@Dao和@Database注解,并生成DAO和Database的实现类,UserDatabase_ImplUserDao_Impl。kapt生成的代码在 build/generated/source/kapt/

133f44c167534fb49e6aaf0df1148800~tplv-k3u1fbpfcp-zoom-1.image

UserDatabase_Impl

``` public final class UserDatabase_Impl extends UserDatabase { private volatile UserDao _userDao;

//RoomDataBase的init中调用 @Override protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) { final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) { @Override public void createAllTables(SupportSQLiteDatabase _db) { //Implementation }

  @Override
  protected void onCreate(SupportSQLiteDatabase _db) {
     //Implementation
  }

});

final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
    .name(configuration.name)
    .callback(_openCallback)
    .build();

final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);

return _helper;

}

@Override protected InvalidationTracker createInvalidationTracker() { final HashMap _shadowTablesMap = new HashMap(0); HashMap> _viewTables = new HashMap>(0); return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "users"); }

@Override public void clearAllTables() { super.assertNotMainThread(); final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase(); try { super.beginTransaction(); _db.execSQL("DELETE FROM users"); super.setTransactionSuccessful(); } finally { super.endTransaction(); _db.query("PRAGMA wal_checkpoint(FULL)").close(); if (!_db.inTransaction()) { _db.execSQL("VACUUM"); } } }

@Override public UserDao userDao() { //实现见后文 } ```

  • createOpenHelperRoom.databaseBuilder().build()创建Database时,会调用实现类的createOpenHelper()创建SupportSQLiteOpenHelper,此Helper用来创建DB以及管理版本
  • createInvalidationTracker :创建跟踪器,确保table的记录修改时能通知到相关回调方
  • clearAllTables:清空table的实现
  • userDao:创建UserDao_Impl

UserDao_Impl

``` public final class UserDao_Impl implements UserDao { private final RoomDatabase __db;

private final EntityInsertionAdapter __insertionAdapterOfUser;

private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;

public UserDao_Impl(RoomDatabase __db) { this.__db = __db; this.__insertionAdapterOfUser = new EntityInsertionAdapter(__db) { //Implementation }; this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter(__db) { //Implementation }; }

@Override public void insertAll(final User... users) { //Implementation }

@Override public void delete(final User user) { //Implementation }

@Override public List getAll() { //Implementation }

@Override public List loadAllByIds(final int[] userIds) { //Implementation }

@Override public User findByName(final String first, final String last) { //Implementation } } ```

UserDao_Impl 主要有三个属性:

  • __db:RoomDatabase的实例
  • __insertionAdapterOfUserEntityInsertionAdapterd实例,用于数据insert。上例中,将在installAll()中调用
  • __deletionAdapterOfUserEntityDeletionOrUpdateAdapter实例,用于数据的update/delete。 上例中,在delete()中调用

RoomDatabase.Builder

Room通过Build模式创建Database实例

val userDatabase = Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).build()

Builder的好处时便于对Database进行配置

  • createFromAsset()/createFromFile() :从SD卡或者Asset的db文件创建RoomDatabase实例
  • addMigrations() :添加一个数据库迁移(migration),当进行数据版本升级时需要
  • allowMainThreadQueries() :允许在UI线程进行数据库查询,默认是不允许的
  • fallbackToDestructiveMigration() :如果找不到migration则重建数据库表(会造成数据丢失)

除上面以外,还有其他很多配置。调用build()后,创建UserDatabase_Impl,并调用init(),内部会调用createOpenHelper()

userDao()

@Override public UserDao userDao() { if (_userDao != null) { return _userDao; } else { synchronized(this) { if(_userDao == null) { _userDao = new UserDao_Impl(this); } return _userDao; } } }

通过构造参数,向UserDao_Impl传入RoomDatabase

insertAll()

@Override public void insertAll(final User... users) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

使用__db开启事务,使用__insertionAdapterOfUser执行插入操作

delete()

@Override public void delete(final User user) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __deletionAdapterOfUser.handle(user); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

同insertAll()

getAll()

@Override public List<User> getAll() { final String _sql = "SELECT * FROM users"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); __db.assertNotSuspendingTransaction(); final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid"); final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name"); final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name"); final List<User> _result = new ArrayList<User>(_cursor.getCount()); while(_cursor.moveToNext()) { final User _item; final int _tmpUid; _tmpUid = _cursor.getInt(_cursorIndexOfUid); final String _tmpFirstName; _tmpFirstName = _cursor.getString(_cursorIndexOfFirstName); final String _tmpLastName; _tmpLastName = _cursor.getString(_cursorIndexOfLastName); _item = new User(_tmpUid,_tmpFirstName,_tmpLastName); _result.add(_item); } return _result; } finally { _cursor.close(); _statement.release(); } }

基于@Query注解的sql语句创建RoomSQLiteQuery,然后创建cursor进行后续操作

数据库升级

当数据库的表结构发生变化时,我们需要通过数据库迁移(Migrations)升级表结构,避免数据丢失。

例如,我们想要为User表增加age字段

| uid | first_name | last_name |

↓↓

| uid | first_name | last_name | age |

数据迁移需要使用Migration类:

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

Migration通过startVersionendVersion表明当前是哪个版本间的迁移,然后在运行时,按照版本顺序调用各Migration,最终迁移到最新的Version

创建Database时设置Migration:

Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).addMigrations(MIGRATION_1_2) .build()

迁移失效

迁移中如果找不到对应版的Migration,会抛出IllegalStateException

java.lang.IllegalStateException: A migration from 1 to 2 is necessary. Please provide a Migration in the builder or call fallbackToDestructiveMigration in the builder in which case Room will re-create all of the tables.

可以添加降级处理,避免crash:

Room.databaseBuilder( applicationContext, UserDatabase::class.java, "users-db" ).fallbackToDestructiveMigration() .build()

  • fallbackToDestructiveMigration:迁移失败时,重建数据库表
  • fallbackToDestructiveMigrationFrom:迁移失败时,基于某版本重建数据库表
  • fallbackToDestructiveMigrationOnDowngrade:迁移失败,数据库表降级到上一个正常版本

集成三方库(LiveData、RxJava等)

作为Jetpack生态的成员,Room可以很好地兼容Jetpack的其他组件以及ACC推荐的三方库,例如LiveData、RxJava等。

使用LiveData

DAO可以定义LiveData类型的结果,Room内部兼容了LiveData的响应式逻辑。

可观察的查询

通常的Query需要命令式的获取结果,LiveData可以让结果的更新可被观察(Observable Queries)。

@Dao interface UserDao { @Query("SELECT * FROM users") fun getAllLiveData(): LiveData<List<User>> }

当DB的数据发生变化时,Room会更新LiveData:

``` @Override public LiveData> getAllLiveData() { final String _sql = "SELECT * FROM users"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return __db.getInvalidationTracker().createLiveData(new String[]{"users"}, false, new Callable>() { @Override public List call() throws Exception { final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid"); final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name"); final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name"); final List _result = new ArrayList(_cursor.getCount()); while(_cursor.moveToNext()) { final User _item; final int _tmpUid; _tmpUid = _cursor.getInt(_cursorIndexOfUid); final String _tmpFirstName; _tmpFirstName = _cursor.getString(_cursorIndexOfFirstName); final String _tmpLastName; _tmpLastName = _cursor.getString(_cursorIndexOfLastName); _item = new User(_tmpUid,_tmpFirstName,_tmpLastName); _result.add(_item); } return _result; } finally { _cursor.close(); } }

@Override
protected void finalize() {
  _statement.release();
}

}); } ```

__db.getInvalidationTracker().createLiveData() 接受3个参数

  • tableNames:被观察的表
  • inTransaction:查询是否基于事务
  • computeFunction:表记录变化时的回调

computeFunction的call中执行真正的sql查询。当Observer首次订阅LiveData时,或者表数据发生变化时,便会执行到这里。

使用RxJava

添加依赖

使用RxJava需要添加以下依赖

``` dependencies { def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version"

// RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" } ```

响应式的查询

DAO的返回值类型可以是RxJava2的各种类型:

  • @Query注解的方法:返回 Flowable 或 Observable.
  • @Insert/@Update/@Delete注解的方法: 返回Completable, Single, and Maybe(Room 2.1.0以上)

``` @Dao interface UserDao { @Query("SELECT * from users where uid = :id LIMIT 1") fun loadUserById(id: Int): Flowable

@Insert
fun insertUsers(vararg users: User): Completable

@Delete
fun deleteAllUsers(users: List<User>): Single<Int>

} @Override public Completable insertLargeNumberOfUsers(final User... users) { return Completable.fromCallable(new Callable() { @Override public Void call() throws Exception { __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); return null; } finally { __db.endTransaction(); } } }); }@Override public Single deleteAllUsers(final List users) { return Single.fromCallable(new Callable() { @Override public Integer call() throws Exception { int _total = 0; __db.beginTransaction(); try { _total +=__deletionAdapterOfUser.handleMultiple(users); __db.setTransactionSuccessful(); return _total; } finally { __db.endTransaction(); } } }); }@Override public Flowable loadUserById(final int id) { final String _sql = "SELECT * from users where uid = ? LIMIT 1"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1); int _argIndex = 1; _statement.bindLong(_argIndex, id); return RxRoom.createFlowable(__db, false, new String[]{"users"}, new Callable() { @Override public User call() throws Exception { //Implementation }

@Override
protected void finalize() {
  _statement.release();
}

}); } ```

如上,使用fromCallable{...}创建Completable与Single; RxRoom.createFlowable{...}创建Flowable。call()里执行真正的sql操作

使用协程Coroutine

添加依赖

使用Coroutine需要添加额外依赖:

``` dependencies { def room_version = "2.2.5"

implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" } ```

挂起函数定义DAO

为UserDao中的CURD方法添加suspend

@Dao interface UserDao { @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 users") suspend fun loadAllUsers(): Array<User> }

CoroutinesRoom.execute 中进行真正的sql语句,并通过Continuation将callback变为Coroutine的同步调用

@Override public Object insertUsers(final User[] users, final Continuation<? super Unit> p1) { return CoroutinesRoom.execute(__db, true, new Callable<Unit>() { @Override public Unit call() throws Exception { __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); return Unit.INSTANCE; } finally { __db.endTransaction(); } } }, p1); }

可以对比一下普通版本的insertUsers:

@Override public void insertUsers(final User... users) { __db.assertNotSuspendingTransaction(); __db.beginTransaction(); try { __insertionAdapterOfUser.insert(users); __db.setTransactionSuccessful(); } finally { __db.endTransaction(); } }

区别很明显,添加了suspend后,生成代码中会使用CoroutinesRoom.execute封装协程。

文末

文章总结了Jetpack Room的基本介绍,以及使用方法。最后源码解析。Android架构师的学习需要很深的学习积累;Android架构师进阶知识学习;希望各位都能成为架构师,顶峰相见!