class: chapter-1, hero, center, middle #
Room
Architecture Components 勉強会 #4 2018/03/19 荒木佑一 --- class: chapter-1, normal # 予定 .card[ [github.com/yaraki/CheeseRoom](https://github.com/yaraki/CheeseRoom) をクローンしておく - これまでのあらすじ - Room 概要 - ハンズオン - Room 機能 解説 - 課題 ] --- class: chapter-2, hero, middle, center # これまでのあらすじ --- class: chapter-2, normal # Lifecycle .card[ Activity や Fragment のライフサイクルを他のオブジェクトでも扱えるようにする どのライフサイクル イベントで 自分が何をすればいいか知っているオブジェクトを作る ] --- class: chapter-2, normal # LiveData .card[ バックグラウンド処理と UI の橋渡し、監視 Lifecycle-aware ] --- class: chapter-2, normal # ViewModel .card[ .large[ ![ViewModel のライフサイクル](viewmodel-lifecycle.png) ] ] --- class: chapter-3, hero, center, middle # Room 概要 --- class: chapter-3, normal # Room とは何か .card[ データベース ライブラリ - アノテーション プロセッサーで処理を生成 - できる限りコンパイル時に検証 - SQL と Java/Kotlin を型安全に対応 - LiveData でのテーブル監視 - テスト - マイグレーション - RxJava もサポート ] --- class: chapter-3, normal # Room とは何でないか .card[ - SQL を隠蔽しない - Java で SQL クエリを組み立てたりはしない - 透過的なバックグラウンド処理はない - クエリは基本的に同期的に実行 - LiveData や RxJava などに任せる - Lazy Load を自動的に行わない ] --- class: chapter-3, normal # Room の基本要素 .card[ - Database - データベース本体 - Entity - テーブル / そのテーブルの 1 行を表す - DAO (Data Access Object) - SQL クエリーと Java/Kotlin メソッドの関連付け ] --- class: chapter-4, hero, center, middle # ハンズオン --- class: chapter-4, normal # CheeseRoom .card[ [github.com/yaraki/CheeseRoom](https://github.com/yaraki/CheeseRoom) モジュール - app-start: ハンズオンの開始地点 (Kotlin) - app-kotlin: 完成版 (Kotlin) - app-java: 完成版 (Java) - common: 画像置き場 (ライブラリ モジュール) - playground: Room の機能を試す用 ] --- class: chapter-4, normal # CheeseRoom: 画面構成 .card[ - MainActivity - CheeseListFragment: リスト画面 - CheeseListViewModel - CheeseDetailFragment: 詳細画面 - CheeseDetailViewModel ] --- class: chapter-4, normal # CheeseRoom: データ層の設計 .card[ ![データ層](final-architecture.png) - CheeseRepository - CheeseApi
Web API - CheeseDatabase
Room のデータベース 各 ViewModel は Repository 経由でデータにアクセス 参照: [Guide to App Architecture](https://developer.android.com/topic/libraries/architecture/guide.html) ] --- class: chapter-4, normal # CheeseRoom: データベース設計 .card[ テーブルは 1 つ - Cheese テーブル - id: 主キー、整数 - name: 名前、文字列 - favorite: お気に入り、真偽値 ] --- class: chapter-4, normal # ハンズオン手順 .card[ 1. Activity と Fragment を読んでおく 2. Cheese.kt (Entity) を完成させる 3. CheeseDatabase.kt を完成させる 4. CheeseDao.kt を完成させる 5. 完成したデータベースを利用して CheeseRepository.kt を完成させる 6. CheeseRepository を利用して 2 つの ViewModel を完成させる ] --- class: chapter-4, hero, center, middle # Android Studio へ ??? ここで作業 --- class: chapter-4, normal # ポイント .card[ - アノテーションから Room が実装を生成 - SQL はビルド時に検証 - データベースと DAO とエンティティー - DAO クエリーから返される LiveData は自動的にテーブルを監視する - 非同期処理は Room では行わない (LiveData 以外) ] --- class: chapter-4, normal # ちなみに .card[ - Room.databaseBuilder(...).build() はメインスレッドで呼んで大丈夫 - CheeseDatabaseTest に簡単なテストのサンプル - メモリー内データベース - Mockito を使って LiveData をテスト - ViewModel や Repository を単体テストするには DI が便利 - 参照: [GithubBrowserSample](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample) ] --- class: chapter-5, hero, center, middle # RoomDatabase --- class: chapter-5, normal # RoomDatabase .card[ ```kotlin @Database(entities = [Cheese::class], version = 1) abstract class CheeseDatabase : RoomDatabase() { abstract fun cheese(): CheeseDao } ``` ] ??? - RoomDatabase を継承した abstract class - @Database アノテーション - エンティティーのリスト - バージョン --- class: chapter-5, normal # RoomDatabase.Builder .card[ ```kotlin Room.databaseBuilder(context, CheeseDatabase::class.java, "cheese.db") .addCallback(...) .addMigrations(...) .build() ``` テスト用 ```kotlin Room.inMemoryDatabaseBuilder(context, CheeseDatabase::class.java) .build() ``` ] ??? - addCallback でコールバックを追加できる - onCreate と onOpen - addMigrations: マイグレーションについては後ほど --- class: chapter-6, hero, center, middle # Entity --- class: chapter-6, normal # Entity の必須要素 .card[ @PrimaryKey が必要 ```kotlin @Entity data class Cheese( `@PrimaryKey` val id: Long, val name: String, val favorite: Boolean) ``` SQL と Java/Kotlin 間の**入出力方法**が必要 ] ??? --- class: chapter-6, normal # 入出力 .card[ Room が使う入出力 - SQL → Java/Kotlin - コンストラクター引数 - セッター (set〜) - Java のメンバー変数が書き込み可能 - Java/Kotiln → SQL - ゲッター (get〜, is〜) - Java のメンバー変数が読み込み可能 ] ??? - CheeseRoom はコンストラクターとゲッター - ゲッター・セッターやコンストラクター引数の対応は名前で解決。 - ビルド時に解決される。リフレクションではない。 - mNantoka にも対応 - Kotlin ならデータ クラスを使えば大丈夫 --- class: chapter-6, normal # テーブル名、カラム名の指定 .card[ ```kotlin @Entity(`tableName = "users"`) data class User( @PrimaryKey(autoGenerate = true) val id: Int, @ColumnInfo(`name = "first_name"`) var firstName: String, @ColumnInfo(`name = "last_name"`) var lastName: String ) ``` ] --- class: chapter-6, normal # 主キーの自動採番 .card[ ```kotlin @Entity(tableName = "users") data class User( @PrimaryKey(`autoGenerate = true`) val id: Int, @ColumnInfo(name = "first_name") var firstName: String, @ColumnInfo(name = "last_name") var lastName: String ) ``` ] ??? - @Insert のとき id が 0 か null なら自動採番 --- class: chapter-6, normal # フィールドの除外 .card[ ```kotlin @Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Int, @ColumnInfo(name = "first_name") var firstName: String, @ColumnInfo(name = "last_name") var lastName: String, `@Ignore` var picture: Bitmap? ) ``` ] --- class: chapter-6, normal # インデックス .card[ ```kotlin @Entity(`indices = [Index(value = ["first_name", "last_name"]`, `unique = true)]`) data class User( @PrimaryKey(autoGenerate = true) val id: Long, @ColumnInfo(name = "first_name") var firstName: String, @ColumnInfo(name = "last_name") var lastName: String } ``` ] --- class: chapter-6, normal # 外部キー制約 .card[ ```kotlin @Entity(`foreignKeys = [ForeignKey(entity = User::class,` `parentColumns = ["id"],` `childColumns = ["userId"])]`) data class Message( @PrimaryKey val id: Long, val userId: Long, val content: String ) ``` ] ??? - テーブル間の整合性が取れない状態を防ぐ --- class: chapter-6, normal # 埋め込み .card[ ```kotlin data class Address(val province: String, val city: String) @Entity data class User( @PrimaryKey val id: Int, val name: String, `@Embedded(prefix = "address_")` val address: Address ) ``` ] ??? - SQLite のテーブルには address_province と address_city ができる --- class: chapter-6, normal # 複合キー .card[ ```kotlin @Entity(primaryKeys = ["category", "code"]) data class Product( val category: String, val code: String, val name: String) ``` または ```kotlin data class ProductId(val category: String, val code: String) @Entity data class Product( @PrimaryKey @Embedded val id: ProductId, val name: String) ``` ] --- class: chapter-7, hero, center, middle # DAO --- class: chapter-7, normal # DAO .card[ DAO = Data Access Object ```kotlin @Dao interface UserDao { // … } ``` あるいは ```kotlin @Dao abstract class UserDao(val db: MyDatabase) { // … } ``` ] ??? - 大体は interface でいい - 抽象クラスの場合 - コンストラクターでデータベースへの参照を受け取ることができる - 実装ありメソッドを追加できる (他のメソッドを組み合わせるなど) --- class: chapter-7, normal # メソッド用のアノテーション .card[ 抽象メソッドに - @Query - @Insert - @Update - @Delete 抽象/具象メソッドに - @Transaction ] --- class: chapter-7, normal # @Insert .card[ ```kotlin @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(message: Message): Long @Insert fun insertAll(messages: List<Message>): List<Long> @Insert fun insertBoth(message: Message, user: User) ``` - 引数: エンティティー、配列、コレクション、可変長 - 返り値: 行 ID (Long) ] ??? - 引数は Entity - 返り値は SQLite の rowId (必ず long) - 同一 Entity の複数 Insert はトランザクションになる - 引数は配列かコレクション (Iterable) か可変長引数 - 返り値は long の配列かリスト - 複数種類の Insert は返り値なし --- class: chapter-7, normal # @Update .card[ ```kotlin @Update fun update(message: Message) @Update fun updateAll(messages: List<Message>) @Update fun updateTogether(message: Message, user: User) ``` - 引数: エンティティー、配列、コレクション、可変長 - 返り値: 影響を受けた行数 (Int) ] ??? - 引数は @Insert と同じ - 返り値は変更された行数 --- class: chapter-7, normal # @Delete .card[ ```kotlin @Delete fun delete(message: Message) @Delete fun deleteAll(messages: List<Message>) @Delete fun deleteTogether(message: Message, user: User) ``` - 引数: エンティティー、配列、コレクション、可変長 - 返り値: 影響を受けた行数 (Int) ] ??? - 引数、返り値は @Update と同じ --- class: chapter-7, normal # @Query .card[ ```kotlin @Query("SELECT * FROM Message") fun all(): List<Message> @Query("SELECT * FROM Message WHERE id = :id") fun byId(id: Long): Message? ``` 使える SQL 文: SELECT、UPDATE、DELETE 引数: SQL 文へのパラメーター 返り値: さまざま
Entity、配列、リスト、プリミティブ、POJO、Cursor、LiveData などなど ] --- class: chapter-7, normal # @Query: カラム選択 .card[ ```kotlin @Entity data class Cheese(@PrimaryKey val id: Long, val name: String, /* 他にたくさんのカラム */) data class CheeseSummary(val id: Long, val name: String) interface CheeseDao { @Query("SELECT id, name FROM Cheese") fun summaryAll(): List<CheeseSummary> } ``` カラムの名前と型さえ一致していれば**エンティティーでなくても**値を受け取る器として利用できる ] --- class: chapter-7, normal # @Query: 複雑なクエリー 1 .card[ ```kotlin @Entity data class User( @PrimaryKey id: Long, val name: String) ``` ```kotlin @Entity(foreignKey = [(entity = User::class, parentColumns = ["id"], childColumns = ["userId"])]) data class Message( @PrimaryKey id: Long, val userId: Long, val content: String) ``` Message を SELECT * したいが、User の name もほしい → INNER JOIN ] --- class: chapter-7, normal # @Query: 複雑なクエリー 2 .card[ ```kotlin data class MessageWithUser(val id: Long, val content: String, @Embedded(prefix = "user_") val user: User) ``` ```kotlin @Query(""" SELECT Message.id , Message.content , Message.timestamp , User.id AS user_id , User.name AS user_name FROM Message INNER JOIN User ON Message.userId = User.id WHERE Message.id = :id """) fun withUser(id: Long): MessageWithUser? ``` ] ??? - @Embedded や @ColumnInfo は Entity でなくても使える --- class: chapter-7, normal # @Query: @Relation .card[ ```kotlin class UserWithMessages { @Embedded lateinit var user: User `@Relation(parentColumn = "id", entityColumn = "userId")` lateinit var messages: List<Message> } ``` ```kotlin @Transaction @Query("SELECT * FROM User WHERE id = :id") abstract fun withMessages(id: Long): UserWithMessages? ``` ] ??? - メッセージテーブルに対するクエリーは Room が自動的に生成 --- class: chapter-7, normal # @Query: UPDATE, DELETE .card[ ```kotlin @Query("DELETE FROM Message WHERE userId = :userId") fun deleteByUserId(userId: Long): Int @Query("UPDATE Message SET content = :content WHERE id = :id") fun edit(id: Long, content: String): Int ``` 返り値: Int / Unit ] --- class: chapter-7, normal # @Query: LiveData .card[ ```kotlin @Query("SELECT * FROM Message WHERE id = :id") fun byId(id: Long): LiveData<Message?> ``` クエリーは非同期 ] ??? --- class: chapter-7, normal # @Transaction .card[ ```kotlin @Dao abstract class UserDao { @Insert abstract fun insert(user: User): Long @Delete abstract fun delete(user: User): Long `@Transaction` fun replace(oldUser: User, newUser: User) { delete(oldUser) insert(newUser) } } ``` ] ??? - --- class: chapter-7, normal # @TypeConverter, @TypeConverters .card[ ```kotlin @Entity data class Message( @PrimaryKey val id: Long, val content: String, val timestamp: `Date`) ``` ```kotlin class DateConverter { @TypeConverter fun fromLong(value: Long?) = if (value == null) null else Date(value) @TypeConverter fun toLong(date: Date?) = date?.time } ``` ```kotlin @Database(entities = [...], version = 1) *@TypeConverters(DateConverter::class) abstract class PlaygroundDatabase : RoomDatabase() { /* ... */ } ``` ] --- class: chapter-8, hero, center, middle # マイグレーション --- class: chapter-8, normal # マイグレーションの前に .card[ ```kotlin @Database(entities = [...], `version = 2`) abstract class SampleDatabase : RoomDatabase() { ... } ``` コード上のデータベース、エンティティー、DAO は最新のスキーマの状態を扱う 過去の状態はコードには現れない ] --- class: chapter-8, normal # スキーマの保存 .card[ app/build.gradle ``` android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } } ``` 1.json, 2. json がエクスポートされる (VCS に入れておく) ] --- class: chapter-8, normal # スキーマの利用 .card[ app/build.gradle ```kotlin android { sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } ``` マイグレーションのテストには過去のスキーマが必要 ] --- class: chapter-8, normal # マイグレーションの指定 .card[ ```kotlin Room.databaseBuilder(context, SampleDatabase::class.java, "s.db") `.addMigrations(MIGRATION_1_2)` .build() ``` ```kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE `Cheese` ...") } } ``` ] --- class: chapter-8, normal # マイグレーションしない .card[ ```kotlin Room.databaseBuilder(context, SampleDatabase::class.java, "s.db") `.fallBackToDestructiveMigration()` .build() ``` 全テーブルを DROP して CREATE し直す ] --- class: chapter-8, normal # テスト .card[ ```kotlin @RunWith(AndroidJUnit4::class) @MediumTest class MigrationTest { @Rule @JvmField val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), SampleDatabase::class.java.canonicalName) @Test fun migrate1To2() { val db1 = helper.createDatabase(DATABASE_NAME, 1) db1.close() val db2 = helper.runMigrationsAndValidate(DATABASE_NAME, 2, true, SampleDatabase.MIGRATION_1_2) db2.close() } } ``` ] ??? - メモリ上データベースでマイグレーションはテストできない。ディスクにアクセスする必要があるため、@MediumTest --- class: chapter-9, normal # 課題 A .card[ 1. playground モジュールにある PlaygroundDatabaseTest を実行 - すべて通るはず 2. PlaygroundDatabase の version を 4 にして再度テスト実行 - 4.json ができる (内容は 3.json と同じ) 3. Entity を追加 - Author (id, name) - Book (id, title, author_id) 4. DAO を追加 - BookDao - INSERT, SELECT など基本的なメソッドを追加、テストを書く ] --- class: chapter-9, normal # 課題 B .card[ 1. DB バージョン 3 から 4 へのマイグレーションを追加 - JSON から CREATE TABLE をコピーするだけ 2. マイグレーションのテスト 3. @Relation や @Embedded を使ってみる - Book の一覧を Author の name 付きで取得 - Author を ID で取得、その Author の Book の一覧も埋め込みで取得 4. LiveData を返すメソッドのテスト ] --- class: chapter-10, normal # Room 1.1.0 .card[ 予定 - Write-Ahead Logging のサポート - DAO の @RawQuery で実行時に組み立てたクエリを使える - RoomDatabaseBuilder#fallBackToDestructiveMigrationFrom(1, 2, 3) - @Transaction が インターフェイスの実装メソッドでも使える (Java 8, Kotlin) - RoomDatabase#clearAllTables() ] --- class: chapter-10, hero, middle, center # ありがとうございました