Sunflowerリポジトリで学ぶJetPack〜Room編

新型コロナの影響で自宅待機になってしまい、その間勉強するものとして`Sunflower`リポジトリを
勧めてもらいました。

JetPackのライブラリのうち、今回は`Room`編です

尚、引用しているソースは明記しているところ以外は、基本的には全て`Sunflower`のリポジトリのものです。　

# 環境
* 確認時は`Android Studio`のバージョンは `3.6.2`を使用しました
* `JetPack`は`AndroidX`ライブラリを利用するので`Compile SDK`を`２８`以上にする必要があります

# そもそも`Room`ってなに？
公式の説明

```
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、
データベースへのより安定したアクセスを可能にし、
SQLite を最大限に活用できるようにします。
```

です！

# `Room`を使う理由

* アプリ内ではオブジェクトを利用して、`SQLite`のデータベースにアクセスできる。
* コンパイル時にチェックができる

(`Sunflower`のリポジトリのトップページの`Room`の説明より）

# 利用する際に必要なこと

必要なことは下記の4点です

1. 依存関係の記載
1. データベース作成
1. エンティティ作成
1. DAO作成

# 依存関係の記載

## `build.gradle`に記載する内容

`build.gradle`には下記の依存関係の記載を行います。
(公式ドキュメントより）

```gradle:build.gradle
    dependencies {
      def room_version = "2.2.3"

      implementation "androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

      // optional - Kotlin Extensions and Coroutines support for Room
      implementation "androidx.room:room-ktx:$room_version"

      // optional - RxJava support for Room
      implementation "androidx.room:room-rxjava2:$room_version"

      // optional - Guava support for Room, including Optional and ListenableFuture
      implementation "androidx.room:room-guava:$room_version"

      // Test helpers
      testImplementation "androidx.room:room-testing:$room_version"
    }
    
```
* 注意点
    * `room-compiler`の部分については、`Kotlin`で使用する場合は`annotationProcessor`ではなく、`kapt`にします
* オプション設定
    * `Kotlin`
    * `RxJava`
    * `Guava`
    * `Test helpers`
        * これらはオプションなので、利用する場合は記載します。

## `Sunflower`リポジトリの場合
`Sunflower`リポジトリの場合はどのようになっているかを見てみましょう

```gradle:build.gradle
buildscript {
    // Define versions in a single place
    ext {
                    :
        // App dependencies
                    :
        roomVersion = '2.1.0'
                    :
    }
                    :
}
```
```gradle:app/build.gradle
dependencies {
    kapt "androidx.room:room-compiler:$rootProject.roomVersion"
                        :
    implementation "androidx.room:room-runtime:$rootProject.roomVersion"
    implementation "androidx.room:room-ktx:$rootProject.roomVersion"
                        :
}
```
`Sunflower`リポジトリの場合は、`Kotlin`のオプションのみ利用しているようです。

# データベースのクラス作成

```kotlin:AppDatabase.kt(part1)
/**
 * The Room database for this app
 */
@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun gardenPlantingDao(): GardenPlantingDao
    abstract fun plantDao(): PlantDao
```
データベースの設定を行うクラスをみてみましょう。
データベース利用の際の設定的なものは、ここの最初の部分に書かれています。

* クラスの宣言に`@Database`アノテーションを記載します（必須)
    * データベースに関連付けられているエンティティのリストをアノテーション内に含む必要があり、`entities`にそれを記載します。(必須）
        * ここでは、`GardenPlanting`クラスと`Plant`クラスが記載されています。
    * `version`はデータベースのバージョンを定義できます。マイグレーション処理を書く際はこちらのバージョンを参照できます(任意)
    * `exportSheme`は`false`に設定しています。(任意)
        * こちらは`default`で`true`に設定されます。
        * `true`に設定された場合は`room.schemaLocation`が`build.gradle`に定義されている場合は、データベースのスキーマ情報をJSONファイルにエクスポートします。それを利用するとマイグレーションのテストが実施できます。
        * 詳しくは[こちら](https://developer.android.com/training/data-storage/room/migrating-db-versions)
* `@TypeConverter`アノテーションは`entity`に保存できない型がある場合（例えば、`GardenPlanting`クラスの`plantDate`は`Calender型)`）、変換処理を実装したクラスをつくり、そのクラス名を記載します。(任意)
    * ここでは、`Converter`クラスに`Calender` ↔︎　`long`　の相互変換処理が記載されており、その`Converter`クラスを記載しています。
* `RoomDatabase`を拡張する抽象クラスとしてクラスを宣言します（必須)
    * `abstract class AppDatabase : RoomDatabase()` として宣言しています。

* 引数が 0 で、`@Dao` アノテーション付きのクラスを返す抽象メソッドをクラス内に記載します(必須)
    * `gardenPlantingDao()`メソッドと`plantDao()`メソッドを記載してあります。



```kotlin:AppDatabase.kt(part2)
    companion object {

        // For Singleton instantiation
        @Volatile private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }
```

こちらは、シングルトンで利用するための内容が記載されています。
すでにインスタンスが作られている場合は、作られているものを使い、ない場合は作るという処理です
スレッドセーフにするために`synchronized（）`で排他制御を行っています。

```kotlin:AppDatabase.kt(part3)
        // Create and pre-populate the database. See this article for more details:
        // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
                    .addCallback(object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
                            WorkManager.getInstance(context).enqueue(request)
                        }
                    })
                    .build()
        }
    }
}
```

こちらでは、データベースを作成する処理です。
* `Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build`でデータベースのインスタンスを作成します。
* その間に`addCallback()`メソッドが入っています。
    * こちらはデータベースが作成された時に実施されるコールバックメソッドです。
    * 例えば、データベースの初期データなどを読み込む必要がある場合などで利用できます。
    * ここでは`WorkManager`を利用し、バックグラウンド処理でjsonファイルから初期値の情報を読み込んで、データベースに保存しています。

# エンティティの定義

それでは、`Sunflower`リポジトリの`GardenPlanting`クラスのエンティティ定義からみてみましょう

```kotlin:GardenPlanting.kt(part1)

@Entity(
    tableName = "garden_plantings",
    foreignKeys = [
        ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
    ],
    indices = [Index("plant_id")]
)
```
* `@Entity`のアノテーションをつけてクラスを作成します。（必須）
    * `tableName`に指定した名称が、`SQLite`データベース内のテーブル名になります。もし、なにも記載しなかった場合はクラス名がテーブル名になります。（任意）
    *  `foreignKeys`は外部キー制約の定義を行います。ここでは、`Plant`クラスの`id`にある値のみ、`plant_id`に設定可能となります。（任意）
    *  `indices`で、このテーブルに設定するインデックスを指定します。この場合は`plant_id`をインデックスに設定しています。（任意）

```kotlin:GardenPlanting.kt(part2)
data class GardenPlanting(
    @ColumnInfo(name = "plant_id") val plantId: String,

    /**
     * Indicates when the [Plant] was planted. Used for showing notification when it's time
     * to harvest the plant.
     */
    @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),

    /**
     * Indicates when the [Plant] was last watered. Used for showing notification when it's
     * time to water the plant.
     */
    @ColumnInfo(name = "last_watering_date")
    val lastWateringDate: Calendar = Calendar.getInstance()
) {
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "id")
        var gardenPlantingId: Long = 0
}
```
* `@ColumnInfo`で各カラムの定義を行います。変数宣言などは通常の場合と同一です。（必須）
* `@ColumnInfo`アノテーションの中の`name`でカラム名を定義します。(任意)
    * テーブル名と同様に、指定しなかった場合は変数名がカラム名となります。
    * ここでは`plantId`,`plantDate`,`lastWateringDate `の３つの引数を指定するコンストラクターを定義しています。
* プライマリーキーには`@PrimaryKey`のアノテーションをつけて変数を定義します。(任意)
    * カラム名`id`、変数名`gardenPlantId`がプライマリーキーに指定されています。
    * ここでは`autoGenerate = true`を指定し、自動的に割り当てるようになっています。

# `DAO`の定義

次に`GardenPlantingDao`をみてみましょう。
`DAO`とは`Data Access Object`の略でデータベースの操作のインターフェースを提供するオブジェクトです。
使う側はデータベースの詳細を知ることなくデータの保存や取得が利用可能です。

```kotlin:GardenPlantingDao.kt(Part1)
/**
 * The Data Access Object for the [GardenPlanting] class.
 */
@Dao
interface GardenPlantingDao {
```
* `@Dao`アノテーションをつけたインターフェースを定義します。(必須）

```kotlin:GardenPlantingDao.kt(Part2)
    @Query("SELECT * FROM garden_plantings")
    fun getGardenPlantings(): LiveData<List<GardenPlanting>>

    @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
    fun isPlanted(plantId: String): LiveData<Boolean>

    /**
     * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle
     * the object mapping.
     */
    @Transaction
    @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
    fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>>
```

* `@Query`アノテーションは、データベースに対して読み書き処理を実行できます。
    * コンパイル時に検証されるので、クエリに問題があると、コンパイルエラーになります
    * 戻り値についても検証を行い、誤りがある場合は警告またはエラーを表示します
    * クエリにパラメータを渡す場合は、`:[メソッドの引数名]`としてクエリ内に記載します
        * ここでは２つめのクエリの中の`:plantId`がそのように使われています
* `@Transaction`アノテーションで記載したメソッドは、全て同一トランザクションで実行します
* 各メソッドの戻り値に`LiveData`を指定することができます。`LiveData`と`Room`の組み合わせで、例えば、データベースのテーブルが更新された際に`View`の更新などができます。詳しくは[LiveData編](http://...)をご覧ください

```kotlin:GardenPlantingDao.kt(Part3)

    @Insert
    suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long

    @Delete
    suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
}
```

* `@Insert`アノテーションをつけたメソッドは、データベースに挿入する実装が`Room`によって作成されます。
    * 複数のパラメータを指定することができます。
    * 全てのパラメータを同一のトランザクションで実行します
    * 戻り値を指定すると、挿入されるアイテムの新しい`rowId` になる`long`の値を返すことができます。
        * `rowId`の詳細については[こちら](https://www.sqlite.org/rowidtable.html)
* `@Delete`アノテーションをつけたメソッドは、こちらも、`@Insert`と同様に、データベースないの指定したエンティティのセットを削除する処理を作成します。
* ここでは使用されていませんが、`@Update`も同様です

* メソッド名の前に`suspend`がついているものは、コルーチン機能を使用した際に非同期かすることができ、メインスレッド上で実行される可能性がなくなります。(サブスレッドで実行して下さい、の意)

# 参考サイト
* [Sunflowerリポジトリ](https://github.com/android/sunflower)
* [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel): 公式ドキュメント
* [LiveData](https://developer.android.com/topic/libraries/architecture/livedata): 公式ドキュメント
* [DataBinding](https://developer.android.com/topic/libraries/data-binding): 公式ドキュメント
* [Room](https://developer.android.com/jetpack/androidx/releases/room): 公式ドキュメント
* [MySQL 外部キ−制約](https://www.atmarkit.co.jp/ait/articles/0707/17/news113_2.html): `@IT`の記事
* [SQLite インデックス](https://www.dbonline.jp/sqlite/index/index1.html): SQLiteのインデックスについて記載されているサイト
* [SQLite rowId](https://www.sqlite.org/rowidtable.html): SQLiteのrowIdについて
* [Room Dao](https://developer.android.com/training/data-storage/room/accessing-data?hl=ja): Room Dao公式ドキュメント
* [Room マイグレーション](https://developer.android.com/training/data-storage/room/migrating-db-versions): Roomマイグレーション