[譯] Room ? Coroutines

Feximin發表於2019-05-07

Illustration by [Virginia Poltrack](https://twitter.com/vpoltrack)

Room 2.1(目前為 alpha 版本)新增了對 Kotlin 協程的支援。DAO 方法現在可以被標記為掛起以確保他們不會在主執行緒執行。預設情況下,Room 會使用架構元件 I/O Executor 作為 Dispatcher 來執行 SQL 語句,但在構建 RoomDatabase 的時候你也可以提供自己的 Executor。請繼續閱讀以瞭解如何使用它、引擎內部的工作原理以及如何測試該項新功能。

目前,Coroutines 對 Room 的支援正在大力開發中,該庫的未來版本中將會增加更多的特性。

給你的資料庫新增 suspense 特性

為了在你的 app 中使用協程和 Room,需將 Room 升級為 2.1 版本並在 build.gradle 檔案中新增新的依賴:

implementation "androidx.room:room-coroutines:${versions.room}"
複製程式碼

你還需要 Kotlin 1.3.0 和 Coroutines 1.0.0 及以上版本。

現在,你可以更新 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)

}
複製程式碼

具有 suspend 方法的 DAO

@Transaction 方法也可以掛起,並且可以呼叫其他掛起的 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

Room 會根據是否在事務內呼叫掛起方法進行區別對待:

1. 事務內

Room 不會對觸發資料庫語句的協程上下文(CoroutineContext)做任何處理。方法呼叫者有責任確保當前不是在 UI 執行緒。由於 suspend 方法只能在其他 suspend 方法或協程中呼叫,因此需確保你使用的 DispatcherDispatchers.IO 或自定義的,而不是 Dispatcher.Main

2. 事務外

Room 會確保資料庫語句是在架構元件 I/O Dispatcher 上被觸發。該 Dispatcher 是基於使處於後臺工作的 LiveData 執行起來的同一 I/O Executor 而建立的。

測試 DAO 掛起方法

測試 DAO 的掛起方法與測試其他掛起方法一般無二。例如,為了測試在插入一個使用者後我們還可以取到它,我們將測試程式碼包含在一個 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)
}
複製程式碼

測試 DAO 的掛起方法

原理

為了能夠了解原理,讓我們看一下 Room 為同步的和掛起的插入方法生成的 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();
  }
}
複製程式碼

Room 對同步插入生成的實現程式碼

再看一下新增 suspend 修飾符後發生的變化:生成的程式碼會確保資料在非 UI 執行緒上被插入。

生成的程式碼傳入了一個 continution 和待插入的資料。使用了和同步插入方法相同的邏輯,不同的是它在一個 Callable#call 方法中執行。

@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);
}
複製程式碼

Room 對掛起插入生成的實現程式碼

不過有趣的是 CoroutinesRoom.execute 方法,這是一個根據資料庫是否開啟以及是否處於事務內來處理上下文切換的方法。

情形 1. 資料庫被開啟同時處於事務內

這種情況下只觸發了 call 方法,即使用者在資料庫中的實際插入操作

情形 2. 非事務

Room 通過架構元件 IO Executor 來確保 Callable#call 中的操作是在後臺執行緒中完成的。

suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
   if (db.isOpen && db.inTransaction()) {
       return callable.call()
   }
   return withContext(db.queryExecutor.asCoroutineDispatcher()) {
       callable.call()
   }
}
複製程式碼

CoroutinesRoom.execute 實現


現在就開始在你的 app 中使用 Room 和協程吧,保證資料庫的操作在一個非 UI 分發器上執行。在 DAO 方法上新增 suspend 修飾符並在其他 supend 方法或者協程中呼叫。

感謝 Chris BanesJose Alcérreca

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章