原文链接:https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5
作者:Florina Muntenescu
Room
从2.1版本(目前已更新到2.2.0-alpha03版本)开始添加了对kotlin
协程的支持。现在我们可以使用suspend
关键字将DAO
中的方法声明为挂起函数,从而保证这些方法不在主线程中执行。请继续阅读以理解如何在Room
中使用协程,它的工作原理,以及如何测试这个新功能。
如果想在你的APP中使用协程来操作Room数据库
,那么必须将项目中的Room
版本升级为2.1版本,同时在build.gradle
文件添加如下依赖:
implementation "androidx.room:room-coroutines:${versions.room}"
另外你的kotlin
版本至少为1.3.0
,Coroutines
版本至少为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
数据库的时候,通过调用setTransactionExecutor
和setQueryExecutor
这两个方法来自定义执行SQL语句的Executor
测试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()
}
}
这种情况会直接执行Callable.call()
方法,也就是数据库的实际插入操作。
这种情况下Room
不会对操作数据库的协程上下文做任何处理,因此调用者需要自己确保调用Room
操作的协程上下文环境不是Dispatcher.Main
这种情况下Room
会确保Callable.call()
操作是在后台线程中完成的。
对于事务和查询Room
会采用不同的Dispatchers
,这些Dispatchers
来源于构建Room数据库
时我们自己自定义的Dispatchers
,或者是系统默认提供的架构组件IO Executor
,这和使LiveData
处于后台运行的Dispatchers
是一样的。
如果有兴趣研究具体的实现原理,可以查看CoroutinesRoom.kt和RoomDatabase.kt的源码
在你的APP中使用Room
和协程吧,保证数据库的操作在non-UI Dispatcher
中执行。使用suspend
将DAO
中的方法定义为挂起函数,并从其他挂起函数或协程中调用他们。