AndroidのRoomでデータベースを操る(Kotlin)

#はじめに
この記事では、軽くRoomに触れつつ実際の使い方を書いています。
ゆるめの理解でとりあえず使いたい！という人向けです。
#Roomって何？
Androidで内部データベースを扱いたい場合は、SQLiteを一般的には使います。
ですが、このSQLiteをそのまま使うことは現在非推奨になっています。理由としては[こちら](https://developer.android.com/training/data-storage/sqlite?hl=ja)の公式ドキュメントによると『クエリはコンパイル時に検証されない』『大量のボイラープレートコードが必要になる』等とのことです。
Roomは、このSQLiteをスマートに使うための物です。
Roomは3つの要素から構成されます。
###Entity
データベースに入れるアイテムの事です。
基本的にはkotlinのdata classを使用します。
###Dao(データアクセスオブジェクト)
データベースに、Entityに関するクエリを与えることが出来るインターフェースです。
ここによく使うクエリを登録します。
###データベース
読んで字のごとく、データベースです。
この中に使用するEntityとDaoを登録します。
#使い方
Roomを使う場合、[androidx](https://developer.android.com/jetpack/androidx?hl=ja)のRoom依存関係をbuild.gradle(app)に記述します。(projectのGradle Scriptの所にあります)
また、Roomを使うのにkaptという物も必要になるので、プログラムの先頭に`apply plugin: "kotlin-kapt"`と記述します。
次に、以下をdependenciesの中に記述します。

```gradle:build.gradle
dependencies{

//..(省略)

    def room_version = "2.2.4"

    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

// 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"
}
```
上記の`def room_version = "2.2.4"`の部分は、[こちら](https://developer.android.com/jetpack/androidx/releases/room#declaring_dependencies)に最新のバージョンがありますので、その時必要に応じて更新して下さい。わざわざ公式ドキュメントを見なくてもIDEなら警告と思うので多分気が付きます。(日本語版だと古いバージョンのままのときがあります。)

次に、先程の三要素を作っていきます。
### Entity
```kotlin:HumanEntity.kt
@Entity
data class HumanEntity constructor(
    @PrimaryKey(autoGenerate = true) val id: Int,
    val name: String,
    val age: Int
)
```
見ての通りで、これは人間のEntityです。idがPrimariyKeyで、他の複数のデータと重複しないものです。また、`autoGenerate = true`とすると、idをこちらから設定しなくても適当に割り振ってくれます。今回はこれを出し入れします。
###Dao
```kotlin:UserDao.kt
@Dao
interface UserDao{
    //データベース上の全てのHumanEntityを取得
    @Query("SELECT * FROM HumanEntity")
    //データベース上にあるhumanNameさんを全て取得
    fun getHumanAll(): List<HumanEntity>
    @Query("SELECT * FROM HumanEntity WHERE name == :humanName")
    fun searchName(humanName: String): List<HumanEntity>
    //humansを全て取得、primaryKey重複の際には入れ替え
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertHuman(vararg humans: HumanEntity)
    //humansを全て消去
    @Delete
    fun deleteAllHuman(vararg humans: HumanEntity)
}
```
このように、`@Query`を使ってSQLiteのクエリを作ったり、`@Insert`や`@Delete`を使って挿入したり削除したりのクエリを作成することが出来ます。
もしクエリが間違っていれば、ここでエラーを吐いてくれます。
その他にも使えるものはあるので、[こちら](https://developer.android.com/training/data-storage/room/accessing-data?hl=ja)を参照して下さい。
###データベース
```kotlin:UserDataBase
@Database(entities = [HumanEntity::class],version = 1)
abstract class UserDataBase: RoomDatabase(){
    abstract fun userDao(): UserDao
}
```
他のEntityを追加したい場合は、[]の中に加えてあげます。
Daoを追加したい場合も、`abstruct fun 〇〇Dao(): 〇〇Dao`と同じように追加します。

それでは、これらを使うプログラムを実際に記述していきます。
機能としては
1. データベース内の全ての人間を表示
3. データベースに人間を登録する
4. データベース内の名前が〇〇である人を全て削除
を作ります。
今回は、TableLayoutとTableRowを使用します。
また、分かりやすくするため、xml内の設定を直打ちしています。(実際にはちゃんとresourceを使ったほうが良いです)
以下コードです。少し長いです。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TableLayout
        android:background="#AAFFFF"
        android:id="@+id/humanTable"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.8">
        <TableRow
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/headRow">
            <TextView
                android:layout_height="wrap_content"
                android:layout_width="200dp"
                android:text="名前"/>
            <TextView
                android:layout_height="wrap_content"
                android:layout_width="200dp"
                android:text="年齢"/>
        </TableRow>
    </TableLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:orientation="vertical"
        android:layout_weight="0.2">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginBottom="20dp">
            <EditText
                android:id="@+id/addNameInput"
                android:layout_width="150dp"
                android:layout_height="match_parent"
                android:inputType="text"
                android:hint="名前"/>
            <EditText
                android:id="@+id/addAgeInput"
                android:layout_width="150dp"
                android:layout_height="match_parent"
                android:inputType="number"
                android:hint="年齢"/>
            <Button
                android:id="@+id/addButton"
                android:layout_width="100dp"
                android:layout_height="match_parent"
                android:text="追加"/>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <EditText
                android:id="@+id/removeNameInput"
                android:layout_width="300dp"
                android:layout_height="match_parent"
                android:inputType="text"
                android:hint="名前"/>
            <Button
                android:id="@+id/removeButton"
                android:layout_width="100dp"
                android:layout_height="match_parent"
                android:text="削除"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>
```
これは、app/res/layout にlayout resources fileとして作って下さい。

```xml:human_row.xml
<?xml version="1.0" encoding="utf-8"?>
<TableRow
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/nameTextView"
        android:layout_height="wrap_content"
        android:layout_width="200dp"
        android:text="名前"/>
    <TextView
        android:id="@+id/ageTextView"
        android:layout_height="wrap_content"
        android:layout_width="200dp"
        android:text="年齢"/>

</TableRow>
```
```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var db: UserDataBase
    private lateinit var dao: UserDao
    private val mainHandler = Handler(Looper.getMainLooper())
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //database という名前のデータベースを使用
        db = Room.databaseBuilder(
            applicationContext,
            UserDataBase::class.java,"database"
        ).build()
        dao = db.userDao()
        setAllHumanToTable()

        addButton.setOnClickListener(ButtonListener())
        removeButton.setOnClickListener(ButtonListener())
    }
    //データベース内にあるデータを全て表示します
    private fun setAllHumanToTable(){
        //データベース関連の動作は重いため、メインスレッドで動かしてはいけない決まりがあります
        AsyncTask.execute{
            val humanList = dao.getHumanAll()
            for(human in humanList){
                //全てのhumanをrowに追加
                val row = View.inflate(this@MainActivity,R.layout.human_row,null)
                row.nameTextView.text = human.name
                row.ageTextView.text = human.age.toString()

                //逆に、メインスレッド以外でUIの操作は出来ません
                //なので、このようにしてメインスレッドにViewの追加をお願いしています
                mainHandler.post{
                    humanTable.addView(row)
                }
            }
        }
    }
    private fun resetTable(){
        //humanTableの子を全て参照して
        //先頭以外のrowを全て削除しています
        humanTable.children.forEach {
            if(it is TableRow && it.id != R.id.headRow){
                mainHandler.post{
                    humanTable.removeView(it)
                }
            }
        }
        setAllHumanToTable()
    }
    private fun addHumanFunction(){
        AsyncTask.execute{
            val name = addNameInput.text.toString()
            val age = addAgeInput.text.toString().toIntOrNull() ?: -1
            if(name != "" && age >= 0){
                dao.insertHuman(HumanEntity(id = 0,name = name,age = age))
            }
        }
    }
    private fun removeHumanFunction(){
        AsyncTask.execute{
            val name = removeNameInput.text.toString()
            val humanList = dao.searchName(name)
            for(human in humanList){
                dao.deleteAllHuman(human)
            }
        }
    }
    inner class ButtonListener:  View.OnClickListener{
        override fun onClick(v: View) {
            AsyncTask.execute {
                when(v.id){
                    R.id.addButton -> addHumanFunction()
                    R.id.removeButton -> removeHumanFunction()
                }
                resetTable()
            }
        }
    }
}
```
見た目はこんな感じになります。
![Screenshot from 2020-02-26 00-26-18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333475/26562ae9-f0fd-d214-22cb-df08c0341533.png)


ソースコードに書いてある部分もありますが、注意点があります。

###メインスレッドでデータベース操作は出来ない
データベースの処理は重いので、メインスレッドで実行してはいけない決まりがあります。もし、メインスレッドで実行してしまうと実行時にエラーが発生します。
なので、非同期処理をするようにしましょう。今回の用に`AsyncTask.excute()`を使うのはあくまで一例です。
###DataBaseオブジェクトを何個も作らない
先程の[こちら](https://developer.android.com/training/data-storage/room?hl=ja)にも書いてありますが、データベースオブジェクトは重いので、シングルトン設計パターンに従うようにして下さい。
#おまけ
今回のコード等は[こちら](https://github.com/ShopOne/RoomSample)にあります。

#参考
https://qiita.com/yukiyamadajp/items/73bffb6a3697cb62f9e1
https://qiita.com/b_a_a_d_o/items/45bda89f49bf163144af
https://developer.android.com/training/data-storage/room?hl=ja
https://developer.android.com/training/data-storage/room/accessing-data?hl=ja