1 of 28

Room と

コルーチンと

Bonfire Android #5

へや

@yuichi_araki

2 of 28

Room 2.2.0-alpha02

AndroidX Versions (http://d.android.com/jetpack/androidx/versions)

  • @Relation@Junction による N:N リレーション
  • @ColumnInfo#defaultValue でカラムの既定値
  • @Insert, @Update, @DeletetargetEntity で一部カラム書き込み
  • (room.incremental: インクリメンタル アノテーション プロセッサー)
  • (room.expandProjection: SELECT * を POJO の内容に従って書き換え)
  • コルーチン Flow

3 of 28

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>

4 of 28

同期的 <-> suspend fun

@Dao�interface BookDao {

@Insertfun insertSync(book: Book)

@Insertsuspend fun insert(book: Book)�}

5 of 28

suspend 利用例

class MyViewModel {

private val _book = MutableLiveData<Book>()

fun saveBook() {� val book = _book.value ?: returnviewModelScope.launch {� db.dao().insert(book)� }� }�}

6 of 28

suspend トランザクション

@Daoabstract class BookDao {� @Insertabstract suspend fun insert(book: Book)� @Deleteabstract suspend fun delete(book: Book)

@Transactionopen suspend fun replace(oldBook: Book, newBook: Book) {� delete(oldBook)� insert(newBook)� }�}

7 of 28

withTransaction

class BookRepository {

suspend fun saveAuthorAndBook(author: Author, book: Book) {� db.withTransaction {� db.author().insert(author)� db.book().insert(book)� }� }�}

8 of 28

Flow の簡単な例

val f = flow {� repeat(10) { emit(it) }�}

runBlocking {� f.collect {� println(it)� }�}

9 of 28

Flow 簡単な入門

コールド ストリーム (<-> Channel: ホット ストリーム)� 中身を取り出そうとするまでコードは実行されない

コンテキスト保持� Dispatcher を指定できる

例外透過� .catch で例外を補足

10 of 28

Flow (2.2.0-alpha02 〜)

@Daointerface BookDao {

@Query("SELECT * FROM book")� fun all(): Flow<List<Book>>

}

11 of 28

Flow -> LiveData (livedata-ktx:2.2?.0-?)

class BookViewModel {� val books = db.book().flowAll().asLiveData()�}

12 of 28

Room のコルーチン

13 of 28

同期的なメソッド

@Daointerface BookDao {� @Query("SELECT * FROM Book")� fun all(): List<Book>�}

Room はどのような実装を生成するか

14 of 28

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

  • SQL 文を実行
  • Cursor から、指定された返り値の型に変換

15 of 28

suspend メソッド

@Daointerface BookDao {� @Query("SELECT * FROM Book")� suspend fun suspendAll(): List<Book>�}

16 of 28

@Overridepublic 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>>() {� @Overridepublic List<Book> call() throws Exception {� // 中略: 中身は同じ� // - SQL を実行� // - Cursor から、指定された返り値の型に変換� }� }, p0);�}

17 of 28

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

18 of 28

Room の�コルーチンと�スレッド

19 of 28

様々なクエリ

@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

20 of 28

Room の非同期 概要

同期的な DAO メソッドの場合 Room はスレッドに関与しない

非同期の場合 (suspend / Flow / LiveData)

読み込み: 並列� ただしジャーナルモードが WAL (デフォルト)のとき

書き込み: 同時にひとつ

21 of 28

実行スレッドをカスタマイズ

Room� .databaseBuilder(context, BookDatabase::class.java, "book.db")� .setQueryExecutor(e1) // 読み込み用� .setTransactionExecutor(e2) // 書き込み用� .build()

TransactionExecutor (書き込み用) に並列の Executor を指定しても同時に 1 スレッドしか使わない

22 of 28

トランザクション

db.runInTransaction { /* … */ }

db.withTransaction { /* … */ } // コルーチン

Room ではすべての書き込みメソッドはトランザクション

Android では SQLite のトランザクションは排他的�同時に一つのスレッドからのみ操作できる

23 of 28

トランザクションとスレッド

val t1 = thread {� db.runInTransaction { /* … */ }�}

val t2 = thread {� db.runInTransaction { /* … */ }�}

t1.join()�t2.join()

24 of 28

トランザクションとスレッド

※ 以下のコードはデッドロックを起こす

db.runInTransaction {� val t = thread {� db.runInTransaction {� // …� }� }� t.join()�}

25 of 28

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.transactionDispatcherelse db.queryDispatcher� return withContext(context) {� callable.call()� }�}

26 of 28

runInTransaction <-> withTransaction

class BookRepository {� fun saveUserAndBookAsync(user: User, book: Book) {� executor.execute {� db.runInTransaction { /* … */ }� }� }

suspend fun saveUserAndBookSuspend(user: User, book: Book) {� db.withTransaction { /* … */ }� }�}

27 of 28

まとめ

Room がコルーチンを�サポート� suspend: 2.1.0 〜� Flow: 2.2.0 〜�スレッドは Room が適切にハンドリング

Lifecycle など他の�コンポーネントも�サポートを強化

28 of 28

ありがとうございました

issuetracker.google.com

@yuichi_araki