vol.1/2 APIのGET結果をキャッシュする (Room+Hilt導入編) 

# APIのGET結果をキャッシュしたい！
天気予報アプリも順調に育ってきました。
「画面を表示」 -> 「APIコール」 -> 「表示」はできましたので、APIコールの回数を減らすべく、10分以内のリクエストならDBを参照する様にしたいと思います。

ということで、キャッシュ先をRoomにすべく、導入していきます。

## Room

### 1. gradleの定義
まずは、Roomの依存定義を行います。

```diff_gradle:app/build.gradle
plugins {
    id 'org.jetbrains.kotlin.android'
+    id 'kotlin-kapt'
    id 'com.google.dagger.hilt.android'
}

android {
    defaultConfig {
+        kapt {
+            arguments {
+                // ① schema定義を出力しておくと、AutoMigrationできるようになるので
+                //    出力先を指定します
+                arg("room.schemaLocation", "$projectDir/schemas")
+            }
+        }
    }
}

dependencies {
+    // Room
+    def room_version = "2.5.2"
+    implementation "androidx.room:room-runtime:$room_version"
+    annotationProcessor "androidx.room:room-compiler:$room_version"
+    implementation "androidx.room:room-ktx:$room_version"
+    kapt "androidx.room:room-compiler:$room_version"
}
```
①「arg("room.schemaLocation", "$projectDir/schemas")」の部分は、後から作成するDatabaseクラスにしている「exportSchema=true」（意味：スキーマ定義をエクスポートするよ）の時に必要になる、スキーマ定義ファイルの出力先を指定しています。

### 2. データエンティティクラスの作成
地域情報（[Areaドメイン](https://github.com/jozuko/WeatherForecast/blob/a581ed4e7e816e38ab1ae31a5937ec8df970794d/app/src/main/java/com/jozu/weatherforecast/domain/Area.kt)）を保存するためのEntity定義をおこないます。
参考：[Room エンティティを使用してデータを定義する by Google developers](https://developer.android.com/training/data-storage/room/defining-data?hl=ja)

<details><summary>Areaのソース</summary><div>

```kotlin:Area
data class Area(
    val centers: List<Center>,
)

data class Center(
    val code: String,
    val name: String,
    val offices: List<Office>,
)

data class Office(
    val code: String,
    val name: String,
)
```
</div></details>

center:office = 1:m　という関係なので、それをRDB的に表現します。

```kotlin:CenterEntity.kt
@Entity(tableName = "center")
data class CenterEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "code") val code: String,
    @ColumnInfo(name = "name") val name: String,
)
```
```kotlin:OfficeEntity
@Entity(tableName = "office")
data class OfficeEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "code") val code: String,
    @ColumnInfo(name = "name") val name: String,
    @ColumnInfo(name = "center_code") val centerCode: String,
)
```

### 3. DAOの作成
[Room DAO を使用してデータにアクセスする by Google develpers](https://developer.android.com/training/data-storage/room/accessing-data?hl=ja)
CenterEntityとOfficeEntityにアクセスするための、AreaDaoクラスを作ります。
Entity:DAO=1:1となるように作るのもありだと思います。

地域情報はマスタデータで、全レコード削除 -> 全レコード挿入という操作を想定しています。

```kotlin:AreaDao
@Dao
interface AreaDao {
    @Query("select * from center " +
           "join office on center.code = office.center_code " +
           "order by center.code, office.code")
    suspend fun getAll(): Map<CenterEntity, List<OfficeEntity>>

    @Query("delete from center")
    suspend fun deleteAllCenter()

    @Query("delete from office")
    suspend fun deleteAllOffice()

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertCenter(centerEntity: CenterEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAllOffice(officeEntityList: List<OfficeEntity>)
}
```
getAll()の部分は、[マルチマップ](https://developer.android.com/training/data-storage/room/accessing-data?hl=ja#multimap)を使用しています。
複数のテーブル定義を格納するためのデータクラスを追加で準備しなくてもいいので、とても便利ですね。（Room 2.4以降です）

@Insert(onConflict = OnConflictStrategy.REPLACE)のonConflictは不要ですが、クラッシュしにくくするために、念のため書いていてます。

### 4. Databaseクラス
Room関連最後のクラス、Databaseクラスを作成します。
```kotlin:AppDatabase.kt
@Database(
    entities = [CenterEntity::class, OfficeEntity::class],
    version = 1,
    exportSchema = true,
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun areaDao(): AreaDao
}
```
versionは、DBのマイグレーション判定に使用するデータベースのバージョンです。
テーブルの追加や削除、カラムの追加、変更、削除など、データベースに関する変更があった場合インクリメントしていきます。

exportSchemaは、マイグレーションが必要な場合に、手動でマイグレーションを記述しないで済むAutoMigrationを使用する際には、必ずtrueにします。この定義と併せて、build.gradleにSchema定義のexport先ディレクトリを指定する記述が必要です。

### 5. HiltのModuleにProviderを追加
HiltのModuleに、DatabaseクラスとDaoクラスのprovider定義を追加します。
```kotlin:InfrastructureModule.kt
    @Singleton
    @Provides
    fun provideAppDatabase(
        @ApplicationContext context: Context,
    ): AppDatabase {
        return Room.databaseBuilder(
            context = context,
            klass = AppDatabase::class.java,
            name = "forecast_db",
        ).build()
    }

    @Singleton
    @Provides
    fun provideAreaDao(
        db: AppDatabase,
    ): AreaDao {
        return db.areaDao()
    }
```

## DAO経由でデータアクセスする
### データ取得
まずは、AreaDao#getAll()を読んでみます。

```diff_kotlin:AreaRepositoryImpl.kt
class AreaRepositoryImpl @Inject constructor(
    private val forecastApi: ForecastApi,
+    private val areaDao: AreaDao,
    private val dispatchers: CoroutineDispatcher,
) : AreaRepository {

+    private fun getAreaFromLocal(): Flow<Future<Area>> {
+        return flow<Future<Area>> {
+            val areaMap = areaDao.getAll()
+            val area = AreaAdapter.adaptFromDb(areaMap)
+            emit(Future.Success(area))
+        }.catch { cause ->
+            emit(Future.Error(cause))
+        }.flowOn(dispatchers)
+    }

}
```

flowの中で、データベースから地域情報を取得し、Adapterを経由して、Areaドメインに変換しています。
<details><summary>AreaAdapterとFutureのソース（興味があれば展開してください）</summary><div>

```kotlin:AreaAdapter.kt
object AreaAdapter {
    fun adaptFromDb(areaEntity: Map<CenterEntity, List<OfficeEntity>>): Area {
        return Area(
            centers = areaEntity.entries.map { (centerEntity, officeEntityList) ->
                Center(
                    code = centerEntity.code,
                    name = centerEntity.name,
                    offices = officeEntityList.map { officeEntity ->
                        Office(
                            code = officeEntity.code,
                            name = officeEntity.name,
                        )
                    }
                )
            },
        )
    }
}
```
```kotlin:Future.kt
sealed class Future<out T> {
    object Idle : Future<Nothing>()
    object Proceeding : Future<Nothing>()
    data class Success<out T>(val value: T) : Future<T>()
    data class Error(val error: Throwable) : Future<Nothing>()
}
```
</div></details>

### データ削除と挿入（トランザクションも使う）
次にデータの削除と挿入をやってみます。
```diff_kotlin:AreaRepositoryImpl.kt
class AreaRepositoryImpl @Inject constructor(
    private val forecastApi: ForecastApi,
+    private val db: AppDatabase,
    private val areaDao: AreaDao,
    private val sharedPref: SharedPref,
    private val dispatchers: CoroutineDispatcher,
) : AreaRepository {
+    private fun saveArea(area: Area): Flow<Future<Area>> {
+        return flow<Future<Area>> {
+            db.withTransaction {
+                areaDao.deleteAllCenter()
+                areaDao.deleteAllOffice()
+
+                area.centers.forEach { center ->
+                    val centerEntity = CenterEntity(code = center.code, name = center.name)
+                    val officeEntityList = center.offices.map { office ->
+                        OfficeEntity(code = office.code, name = office.name, centerCode = center.code)
+                    }
+
+                    areaDao.insertCenter(centerEntity)
+                    areaDao.insertAllOffice(officeEntityList)
+                }
+            }
+            emit(Future.Success(area))
+        }.catch { cause ->
+            emit(Future.Error(cause))
+        }.flowOn(dispatchers)
+    }
}
```
同一トランザクジョンで、データの削除と挿入を行う場合は、「withTransaction」を使用します。
withTransactionはsuspend関数です。
not suspendの時は、「runInTransaction」を使用します。
（ちなみに、withTransactionは、「androidx.room:room-ktx」への依存関係が必要になります。）

Insertの際にエンティティのインスタンスを作成しますが、今回、エンティティに定義してるidはAutoGeneratedです。
その場合、idには0を指定するのがお約束です。
[リファレンス by Google developers](https://developer.android.com/reference/androidx/room/PrimaryKey#getAutoGenerate())
```kotlin:CenterEntity.kt
data class CenterEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
}
```

## 完成
完成コードは[こちら](https://github.com/jozuko/WeatherForecast/tree/a581ed4e7e816e38ab1ae31a5937ec8df970794d)です。

[vol.2/2 APIのGET結果をキャッシュする (FlowChain,例外処理編)](https://qiita.com/jozuko_dev/items/e3c71b01df200f6924f7)