【译】Room Coroutines

原文链接: https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5

原文链接:https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5
作者:Florina Muntenescu

Room从2.1版本(目前已更新到2.2.0-alpha03版本)开始添加了对kotlin协程的支持。现在我们可以使用suspend关键字将DAO中的方法声明为挂起函数,从而保证这些方法不在主线程中执行。请继续阅读以理解如何在Room中使用协程,它的工作原理,以及如何测试这个新功能。

给你的数据库添加suspend特性

如果想在你的APP中使用协程来操作Room数据库,那么必须将项目中的Room版本升级为2.1版本,同时在build.gradle文件添加如下依赖:

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

另外你的kotlin版本至少为1.3.0Coroutines版本至少为1.0.0

现在你可以将DAO中的方法使用suspend关键字将其定义为挂起函数了

//具有suspend方法的DAO代码示例
@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声明的方法也可以定义为挂起函数,同时它也可以调用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)
}

你也可以在事务中调用不同DAO的挂起函数

//在一个事务中调用两个不同DAO的挂起函数
class Repository(val database: MyDatabase) {

    suspend fun clearData(){
        database.withTransaction {
            database.userDao().deleteLoggedInUser() // suspend function
            database.commentsDao().deleteComments() // suspend function
        }
    }    
}

默认情况Room会使用架构组件的IO Executor来执行SQL语句,但我们也可以在构建Room数据库的时候,通过调用setTransactionExecutorsetQueryExecutor这两个方法来自定义执行SQL语句的Executor

测试DAO中的挂起函数

测试DAO中的挂起函数和测试其他普通的挂起函数没有什么不同。

例如我们要测试用户信息被插入后能够被检索到,我们可以将测试代码块放在runBlock块中:

@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支持协程的实现原理,我们来看看Room为同步方法和挂起函数生成的DAO方法实现

//DAO中同步和挂起函数的定义插入操作

@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)

对于同步插入,生成的代码首先开启了一个事务,接着执行数据插入操作,然后标记事务成功,最后结束事务。生成的代码如下:

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

从上面的代码也可以看出来在任何线程中调用同步插入方法都会在该线程中直接执行插入操作。

接下来让我们来看看使用了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);
}

上面的生成代码确保了插入操作不会在UI线程中执行。
在生成的suspend函数的中,传入了一个Continuation和待插入的数据,同时它的插入逻辑和同步插入逻辑相同,只是它的插入逻辑被封装在Callable中。

另外我们可以看到,生成的函数一开始会调用CoroutinesRoom.execute函数,实际上该函数会根据数据库是否打开,是否处于事务来决定如何切换上下文。

CoroutinesRoom.execute方法实现如下:

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

情况1:数据库打开且处于事务中

这种情况会直接执行Callable.call()方法,也就是数据库的实际插入操作。

这种情况下Room不会对操作数据库的协程上下文做任何处理,因此调用者需要自己确保调用Room操作的协程上下文环境不是Dispatcher.Main

情况2:非事务

这种情况下Room会确保Callable.call()操作是在后台线程中完成的。

对于事务和查询Room会采用不同的Dispatchers,这些Dispatchers来源于构建Room数据库时我们自己自定义的Dispatchers,或者是系统默认提供的架构组件IO Executor,这和使LiveData处于后台运行的Dispatchers是一样的。

如果有兴趣研究具体的实现原理,可以查看CoroutinesRoom.kt和RoomDatabase.kt的源码


在你的APP中使用Room和协程吧,保证数据库的操作在non-UI Dispatcher中执行。使用suspendDAO中的方法定义为挂起函数,并从其他挂起函数或协程中调用他们。


下面是我的个人公众号,欢迎关注交流
【译】Room Coroutines_第1张图片

你可能感兴趣的:(译)