前言

本文翻译自 Florina MuntenescuRoom 🔗 Coroutines 。介绍了 Google 官方对 Room 提供了原生的 Coroutines 支持。
从 Room v2.1.0 开始,我们可以使用 suspend 标记 DAO 中的函数,确保在非主线程中操作数据库。

如何使用?

build.gradle 中添加依赖库:(最新版本可以在官方更新文档中查看)

implementation "androidx.room:room-ktx:${versions.room}"

需要使用 Kotlin 1.3.0+ 和 Coroutines 1.0.0+。

然后我们可以在 DAO 中使用 suspend 函数:

@Dao
interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsers(): List<User>

    @Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
    suspend fun incrementUserAge(userId: String)

    @Insert
    suspend fun insertUser(user: User)

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)
}

Transaction 方法也可以被 suspend 标记,并且可以调用其它的 suspend 函数。

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

我们也可以在一个 transaction 内调用不同 DAO 中的 suspend 函数。

class Repository(val database: MyDatabase) {
    suspend fun clearData(){
        database.withTransaction {
            database.userDao().deleteLoggedInUser() // suspend function
            database.commentsDao().deleteComments() // suspend function
        }
    }    
}

此外,我们可以在构建 database 时通过调用 setTransactionExecutorsetQueryExecutor 传入指定的 Executor 来控制这些 suspend 函数的协程调度器。如果不设置,它们默认都会在 query 操作执行的子线程中。

注意:suspend 不能与 RxJavaLiveData 共用。 因此下面的写法会在编译期报错。

@Dao
interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsersWithFlowable(): Flowable<List<User>>

    @Query("SELECT * FROM users")
    suspend fun getUsersWithLiveData(): LiveData<List<User>>
}

如何测试?

DAO 中的 suspend 函数的测试与其它的 suspend 测试没什么不同。举个例子,下面我们测试 insert 一条 User 数据,然后验证是否能 query 相同的 User。我们可以借助 runBlocking 进行测试:

@Test fun insertAndGetUser() = runBlocking {
    // Given a User that has been inserted into the DB
    userDao.insertUser(user)

    // When getting the Users via the DAO
    val usersFromDb = userDao.getUsers()

    // Then the retrieved Users matches the original user object
    assertEquals(listOf(user), userFromDb)
}

源码分析

我们知道,Room 编译器会为我们自动生成 DAO 的默认实现,下面就来看一下 suspend 函数和普通函数生成的代码有什么区别。我们首先定义这两个函数,如下:

@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)

普通函数 insertUserSync 生成的代码如下所示:

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

可以看到,它的实现里首先开启了一个事物(transaction),然后执行 insert 操作,并将事务置为成功,最后关闭事务。

下面我们再来看下 suspend 函数的实现:

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

如上所示,suspend 函数内部通过 Callable 包装了与普通 insert 函数一样的逻辑。不同的是,调用了一个 suspend 函数 – CoroutinesRoom.execute ,它内部切换到子线程来执行。
我们来看看 CoroutinesRoom 的源码:

class CoroutinesRoom private constructor() {

    companion object {

        @JvmStatic
        suspend fun <R> execute(
            db: RoomDatabase,
            inTransaction: Boolean,
            callable: Callable<R>
        ): R {
            if (db.isOpen && db.inTransaction()) {
                return callable.call()
            }

            // Use the transaction dispatcher if we are on a transaction coroutine, otherwise
            // use the database dispatchers.
            val context = coroutineContext[TransactionElement]?.transactionDispatcher
                ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
            return withContext(context) {
                callable.call()
            }
        }
    }
}

/**
 * Gets the query coroutine dispatcher.
 *
 * @hide
 */
internal val RoomDatabase.queryDispatcher: CoroutineDispatcher
    get() = backingFieldMap.getOrPut("QueryDispatcher") {
        queryExecutor.asCoroutineDispatcher()
    } as CoroutineDispatcher

/**
 * Gets the transaction coroutine dispatcher.
 *
 * @hide
 */
internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher
    get() = backingFieldMap.getOrPut("TransactionDispatcher") {
        queryExecutor.asCoroutineDispatcher()
    } as CoroutineDispatcher

查看源码可知:

  1. 当数据库被打开并正在执行事务时:会直接调用 Callable#call() 函数执行 insert 操作。
  2. 非上述情况时:Room 要确保 Callable#call() 中的操作要在子线程中执行。Room 会使用不同的协程调度器执行 transactionquery。我们可以在构建 Database 时使用 setTransactionExecutorsetQueryExecutor 配置;若不配置默认会使用 Architecture Components 提供的 IO 线程。这个线程也是 LiveData 执行后台任务的线程。

完整的源码参见:CoroutinesRoom.javaRoomDatabase.kt

尽情的在 Room 中使用 Coroutines 吧,现在已经是 Release 版本了,它可以内部保证数据库操作运行在非 UI 调度器,使用 suspend 可以像同步调用一样完成数据库的读写。

Reference

联系

我是 xiaobailong24,您可以通过以下平台找到我: