Room と
コルーチンと
私
Bonfire Android #5
へや
@yuichi_araki
Room 2.2.0-alpha02
AndroidX Versions (http://d.android.com/jetpack/androidx/versions)
suspend fun (2.1.0 〜)
@Insert�suspend fun insert(book: Book)
@Update�suspend fun update(book: Book)
@Delete�suspend fun delete(book: Book)
@Query("SELECT * FROM Book")�suspend fun all(): List<Book>
同期的 <-> suspend fun
@Dao�interface BookDao {
@Insert� fun insertSync(book: Book)
@Insert� suspend fun insert(book: Book)�}
suspend 利用例
class MyViewModel {
private val _book = MutableLiveData<Book>()
fun saveBook() {� val book = _book.value ?: return� viewModelScope.launch {� db.dao().insert(book)� }� }�}
suspend トランザクション
@Dao�abstract class BookDao {� @Insert� abstract suspend fun insert(book: Book)� @Delete� abstract suspend fun delete(book: Book)
@Transaction� open suspend fun replace(oldBook: Book, newBook: Book) {� delete(oldBook)� insert(newBook)� }�}
withTransaction
class BookRepository {
suspend fun saveAuthorAndBook(author: Author, book: Book) {� db.withTransaction {� db.author().insert(author)� db.book().insert(book)� }� }�}
Flow の簡単な例
val f = flow {� repeat(10) { emit(it) }�}
runBlocking {� f.collect {� println(it)� }�}
Flow 簡単な入門
コールド ストリーム (<-> Channel: ホット ストリーム)� 中身を取り出そうとするまでコードは実行されない
コンテキスト保持� Dispatcher を指定できる
例外透過� .catch で例外を補足
Flow (2.2.0-alpha02 〜)
@Dao�interface BookDao {
@Query("SELECT * FROM book")� fun all(): Flow<List<Book>>
}
Flow -> LiveData (livedata-ktx:2.2?.0-?)
class BookViewModel {� val books = db.book().flowAll().asLiveData()�}
Room のコルーチン
同期的なメソッド
@Dao�interface BookDao {� @Query("SELECT * FROM Book")� fun all(): List<Book>�}
Room はどのような実装を生成するか
@Override�public List<Book> all() {� final String _sql = "SELECT * FROM Book";� final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);� __db.assertNotSuspendingTransaction();� final Cursor _cursor = DBUtil.query(__db, _statement, false, null);� try {� final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");� final int _cursorIndexOfTitle = CursorUtil.getColumnIndexOrThrow(_cursor, "title");� final List<Book> _result = new ArrayList<Book>(_cursor.getCount());� while(_cursor.moveToNext()) {� final Book _item;� final long _tmpId;� _tmpId = _cursor.getLong(_cursorIndexOfId);� final String _tmpTitle;� _tmpTitle = _cursor.getString(_cursorIndexOfTitle);� _item = new Book(_tmpId,_tmpTitle);� _result.add(_item);� }� return _result;� } finally {� _cursor.close();� _statement.release();� }�}
suspend メソッド
@Dao�interface BookDao {� @Query("SELECT * FROM Book")� suspend fun suspendAll(): List<Book>�}
@Override�public Object suspendAll(final Continuation<? super List<Book>> p0) {� final String _sql = "SELECT * FROM Book";� final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);� return CoroutinesRoom.execute(__db, false, new Callable<List<Book>>() {� @Override� public List<Book> call() throws Exception {� // 中略: 中身は同じ� // - SQL を実行� // - Cursor から、指定された返り値の型に変換� }� }, p0);�}
@JvmStatic�suspend fun <R> execute(� db: RoomDatabase, inTransaction: Boolean, callable: Callable<R>�): R {� if (db.isOpen && db.inTransaction()) {� return callable.call()� }
val context = coroutineContext[TransactionElement]� ?.transactionDispatcher� ?: if (inTransaction) db.transactionDispatcher� else db.queryDispatcher� return withContext(context) {� callable.call()� }�}
Room の�コルーチンと�スレッド
様々なクエリ
@Query("SELECT * FROM Book")�fun all(): List<Book>
@Query("SELECT * FROM Book")�suspend fun suspendAll(): List<Book>
@Query("SELECT * FROM Book")�fun flowAll(): Flow<List<Book>>
@Query("SELECT * FROM Book")�fun liveDataAll(): LiveData<List<Book>>
同期
非同期
一回
監視
UI
Room の非同期 概要
同期的な DAO メソッドの場合 Room はスレッドに関与しない
非同期の場合 (suspend / Flow / LiveData)
読み込み: 並列� ただしジャーナルモードが WAL (デフォルト)のとき
書き込み: 同時にひとつ
実行スレッドをカスタマイズ
Room� .databaseBuilder(context, BookDatabase::class.java, "book.db")� .setQueryExecutor(e1) // 読み込み用� .setTransactionExecutor(e2) // 書き込み用� .build()
TransactionExecutor (書き込み用) に並列の Executor を指定しても同時に 1 スレッドしか使わない
トランザクション
db.runInTransaction { /* … */ }
db.withTransaction { /* … */ } // コルーチン
Room ではすべての書き込みメソッドはトランザクション
Android では SQLite のトランザクションは排他的�同時に一つのスレッドからのみ操作できる
トランザクションとスレッド
val t1 = thread {� db.runInTransaction { /* … */ }�}
val t2 = thread {� db.runInTransaction { /* … */ }�}
t1.join()�t2.join()
トランザクションとスレッド
※ 以下のコードはデッドロックを起こす
db.runInTransaction {� val t = thread {� db.runInTransaction {� // …� }� }� t.join()�}
Room が書き込みコルーチンを実行するとき
@JvmStatic�suspend fun <R> execute(db: RoomDatabase, inTransaction: Boolean, callable: Callable<R>): R {� if (db.isOpen && db.inTransaction()) { return callable.call() }
val context = coroutineContext[TransactionElement]� ?.transactionDispatcher� ?: if (inTransaction) db.transactionDispatcher� else db.queryDispatcher� return withContext(context) {� callable.call()� }�}
runInTransaction <-> withTransaction
class BookRepository {� fun saveUserAndBookAsync(user: User, book: Book) {� executor.execute {� db.runInTransaction { /* … */ }� }� }
suspend fun saveUserAndBookSuspend(user: User, book: Book) {� db.withTransaction { /* … */ }� }�}
まとめ
Room がコルーチンを�サポート� suspend: 2.1.0 〜� Flow: 2.2.0 〜�スレッドは Room が適切にハンドリング
Lifecycle など他の�コンポーネントも�サポートを強化
ありがとうございました
issuetracker.google.com
@yuichi_araki