[Android] お寿司屋で例えるAndroid Architecture Compoment 第五貫:Room


この記事は

[Android] お寿司屋で例えるAndroid Architecture Compoment 第四貫:Coroutinesの続きです。

おさらい

MVVMをこんな感じでお寿司屋に例えたとさ

View(Activity/Fragment)はお客さん

ViewModelが板前さん

Repositoryが、漁師

Modelが、海

MVVMを海からお魚を取ってきてお客さんに提供するまでの役割を分割したものと捉えています。
詳しくは、第一貫をご覧ください。(5分位で読める内容ですよ!)

こんな感じのアプリを作ったとさ

今回このシリーズを通して実装する事は、お寿司屋で例えるAndroid Architecture Compoment ということで、アプリ自体も先ほど共有したお寿司屋さんの流れを踏襲します。
こんな感じのアプリ作りまーす!

マグロしか食べてなかったり、小学生女子が下校途中にお寿司屋に寄っていたりとツッコミどころがありますが、スルーしてください。
先程のMVVMの例えと、アプリの例えが少し違いがありますが、こちらもスルーで。

View(お客さん)

注文画面を表示

ViewModel(板前さん)

注文画面のデータや処理を保持

Model

注文履歴を保持

本題

どうもReoです。
今回はAndroidのDatabaseで良く使われるライブラリ、Roomについて解説します。
MVVMのModel、つまり海の部分に当たります。

Roomとは?

ドキュメントを見てみましょう

Roomを使用してローカル データベースにデータを保存する

データベースというのは、データを保存する「箱」みたいなものですね。
普通アプリの情報は、何もしなければ基本的に使い捨てです。
つまり、アプリを閉じればそこで処理された情報等は、初期化されて消えてしまいます。

しかし、例えばゲームアプリを作るとして、そこで発生するスコアを保存したい!
と思ったらデータベースの出番です。

データベースは、大きく二種類に分けることが出来ます。
まず、ローカルデータベースとリモートデータベース。
この二つは、対比関係にあります。
※以下データベースをDBを略称します。

二つとお寿司屋的に例えてみましょう。

魚をデータとすると、家の冷蔵庫(ローカルDB)と海(リモートDB)となります。
ローカルDBは、家の冷蔵庫の様に自分だけが保持するものです。
その為、自分だけが見る情報を保存するのに適しているのかな?

リモートデータベースは、海の様にデータをみんなと共有することが出来ます。
海は、インターネットに当たります。

つまり、リモートはネット上にあり、ローカルは自分の端末のアプリ内にしかありません。
どちらもアプリを閉じてもデータベースに保存したデータは残るのですが、ローカルは、アプリをアンインストールするとデータベースごと消えてしまします。また、リモートはネットに接続してないとデータを見ることが出来ません。

しかし、データベースはただのデータを入れる「箱」なのですが、使い方があります。
SQLiteという言語を用いて操作せねばならんのです。
AndroidもSQLiteをサポートしていて、使う事が出来ますが、知り合いの漁業協同組合の方に聞いたところ、結構面倒らしいです。

下記は、SQLiteについてのドキュメントなのですが、あまりお勧めしてませんね。

上記の API は強力ですが、極めてローレベルであるため、活用するにはかなりの時間と労力が必要となります。
RAW SQL クエリはコンパイル時に検証されません。データグラフに変更があった場合、影響を受ける SQL クエリを手動でアップデートする必要があります。これは、時間がかかり、エラーも発生しやすいプロセスです。
SQL クエリとデータ オブジェクトを変換するには、大量のボイラープレート コードを記述する必要があります。
そのため、アプリの SQLite データベース内の情報にアクセスするための抽象化レイヤとして Room 永続ライブラリを使用することを強くおすすめします。

最後の一文にあるように「強く」おすすめしているので、Roomを使うのが良さそうです。
つまり、Roomはローカルデータベースであり、また最近出てきた技術でもあり、Google自体が推奨しているくらい性能が良いので、「最新冷蔵庫」という認識でいいと思います。

使ってみる

導入

依存関係を追加して使えるようにします。

build.gradle(app
dependencies {
  def room_version = "2.2.6"

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

  implementation "androidx.room:room-ktx:$room_version"

  testImplementation "androidx.room:room-testing:$room_version"
}

Roomの構成について

・データベース
・エンティティ
・Dao

データベースは、魚を入れる「冷蔵庫」という認識で。

エンティティは、「仕切り」みたいなものです。
データベースは、ただ単にお魚を放り投げて保存しておくのでは、なく中身はきちんと整理されています。
ソフトウェアの世界なので目には見えないのですが、視覚で捉えると表形式で見る事が多いですね。

エンティティは、上記の表のことで、具体的には商品IDや商品名といった項目の部分を定義します。
冷蔵庫的には、卵はここ、魚はここ、しょうゆはここに入れる…といった具合に整理しやすくするための「仕切り」だということですね。

続いて、Dao
これは、シンプルで「操作」です。
データベースには、データを保存することはもちろん、取り出す、削除する、更新するの通称CRUDを行います。
それを定義するのがDaoです。

書き方

それでは、ドキュメントに掲載されているコードを元に解説していきましょうか。

MyDatabase.kt
@Entity
data class Sushi(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val orderHistory: Int,
    val price: Int
)

@Entityというアノテーションを付けると、エンティティとしての役割を持ちます。
アノテーションとは、注釈という意味で、何かに対して「意味付け」をします。
この場合は、Suhiというデータクラスにエンティティという「意味付け」をしています。

ここでやっていることは、Sushiという「表」を作り、order_historyと、priceという項目を作っています。
注文履歴と値段を保存したいのですね。

@Primary(プライマリーキー)とは、日本語で主キーと言って、「出席番号」(1,2,3,4,....といった具合に)のようなものです。
保存する情報一つ一つに一意の番号を割り当てます。
まさに、「出席番号」!
ちなみに、アノテーションにautoGenerate = trueを追加すると、「出席番号」を自動で割り当ててくれます。

MyDatabase.kt
   @Dao
    interface SushiDao {
        @Query("SELECT * FROM sushi")
        fun getAll(): List<Sushi>

        @Query("SELECT * FROM sushi where id = :id")
        fun getHistory(id: Int): Sushi

        @Insert
        fun insertSushi(sushi: Sushi)

        @Delete
        fun delete(sushi: Sushi)
    }
MyDatabase.kt
    @Database(entities = arrayOf(Sushi::class), version = 1)
    abstract class SushiDatabase : RoomDatabase() {
        abstract fun suhiDao(): SushiDao
    }

これで、データベースはほぼ完成したようなものです。

まず、Daoを見ていきましょう。
@Query(クエリ)このアノテーションは命令文を書くよーの「意味付け」です。
SELECTは、英語のままで「選ぶ」。
*は、「全部」。
FROMは、「~から」
sushiは、データベースのエンティティ(仕切り)の名前。

つまり、最初のgetAllは保存されたデータを全部取り出す操作を定義しています。

次getHistoryは、where id = :idが追加されていますね。
whereは、条件を指定します。
引数にidを持つので、勘の良い人は分かったかもしれません。
つまり、3を渡すと3番目に保存したデータを取り出す事が出来ます。
getAllの様に全部でなく、個別に取り出したいときに呼び出します。

残りの二つは、簡単です。
@Insert@delete、この二つのアノテーションは、Google翻訳を用いたら分かると思いますw
データの挿入と、削除を行っています。

では、@Databaseはデータベースだよぉー!という意味付けをしています。(雑w)
entities = でエンティティとして定義したデータクラス(Sushi)をリスト型でセット、そしてversionにてバージョンの数値を指定します。初めてなので1です。
SushiDatabaseという名前でRoom(最新冷蔵庫)を継承して、Daoを宣言したら完成です!

最新冷蔵庫(Room)は、完成しましたが、アプリ内にはないので発送してあげましょう。
新しくApplicationクラスを作って

MyApplication.kt
class MyApplication : Application() {

      Companion Object {
         lateinit var db: MyDatabase
      }

      override fun onCreate() {
         super.onCreate()

         db = Room.databaseBuilder(
             this,
             SushiDatabase::class.java,
             "Room-Database"
         ).build()
      }

}

その後はManifestを開き、applicationタグ内にandriod:name属性を追加して、MyApplicationを指定します。

これで、アプリ内でRoomデータベースが使えるようになりました。

Companion Objectは、シングルトンといって実行した時にインスタンス生成を一つに制約するためのものです。
RoomDBはアプリを閉じても残り、何個もインスタンス必要ないですよね!その為、Companionで宣言します。
ちなみに、Objectは省略可能です。

databaseBuilderの引数は、context(詳しくはこちら)とklass(データベース自体)とname(データベースの名前で、適当で良い)。
.build()で、締めます。

使い方

HogeHoge.kt
    // クラス直下に宣言
    private val sushiCrud = MyApplication.db.sushiDao()
    ...
    lifecycleScope.launch(Dispathers.IO) {
         val historyAll = sushiCrud.getAll
         Log.d("debug",historyAll)
    }
    ...

前回の記事で、説明したのですがデータベースの操作をするときは、メインスレッドではなくIOスレッドで行います。
launch(Dispathers.IO)で、スレッドを指定できます。

次で貫結!!!