Android
Kotlin

KotlinでRoomを使う

今年は Room(a SQLite object mapping library) を Kotlinで扱うとどうなる、の話です。
基本的にはkaptを追加記述するだけなんですが、、一部kotlinだと楽になる部分もありますのでぜひRoomはkoltinでやっていきましょう。
本気で解説してもらってる記事には負けますが、実需がある程度簡単にRoomについても触れながらどう扱うかを書いていきます。

Room?

Room Persistence Library 導入和訳

AndroidフレームワークはSQLiteをサポートしてる、こいつそのものはパワフルだけど、低レイヤーだから使いこなすには労力が必要だよね。

Roomを使うことで端末がインターネットに接続しているかどうか関係なくキャッシュの保存が可能(インターネットの環境に復帰した際に同期される)

SQLiteクエリはコンパイル時には検証されないため、必要なとき(スキーマが変更された等)開発者がクエリを変更する必要があり、バグの原因になりやすい。
しかもデータオブジェクト変更のためにお作法コードもめちゃくちゃ書かなきゃいけない。

Roomではこういう部分に抽象化レイヤーを用意することで対応したよ。

導入

gradleに

compile "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
kapt "android.arch.persistence.room:compiler:1.0.0"

kaptを追加したのみで、その他はjavaと同じです。

Roomの構成

room_architecture.png
引用-AndroidDeveloper
三つの要素がRoomを構成しています。

  • entity
  • DataAccessObject
  • Room datebase

の3つです。
それぞれ用意して呼び出します。

Entityを用意して、DataAcessObjects(Dao)で操作を記述という形です。

Entity

@Entity
data class Address constructor(
        @PrimaryKey(autoGenerate = true)
        val id: Long,
        val address: String,
        val name: String
)

@Entity Annotationを付けることでこのクラスがデータベースに保存されるEntityとなります。この辺はRealmとかと同じですね。
また、DataAccessObjectからこのEntityは見える必要があるのでgetter/setterを用意する必要があるわけですが、dataObjectにすることで省略することができるのが利点ですね。

@PrimaryKey をオブジェクトに一つはつける必要があります。

autoGenerate でてきとうなプライマリーキーを発行してくれます。

今回使わなかったのですが、

@ColumnInfo(name = "place_id") val id : Long

等やるとSQLを叩くときにはスネークケース、実際にコードを書くときはキャメルケースにすることができるわけです。

DateAccessObjects

コード上では長いのでDaoと記述しました。

@Dao
interface AddressDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun createAddress(address: Address)

    @Query("SELECT * FROM Address")
    fun findAll(): List<Address>

    @Update
    fun updateAddress(address: Address)

    @Delete
    fun delete(address: Address)
}

Daoを書いてみました。

Annotationは全部で4つ。

  • Insert
  • Update
  • Delete
  • Query

です。そのままの意味ですが、@Query ではSQL文をそのまま記述することができます。今回は findAll() なのでそのようなSQL文を書いてあります。(この部分は流石にどうしようもなかったか)

Insertに onConflict に書いてありますが、コンフリした際に指定できるのが5つ。

  • ABORT
  • FAIL
  • IGNORE
  • REPLACE
  • ROLLBACK

です。そのままです。今回はREPLACEしてもらえればいいのでそのようにしています。

Datebase

@Database(entities = arrayOf(AddressDao::class), version = 1)
abstract class DataBase : RoomDatabase() {
    abstract fun addressDao(): AddressDao
}

arrayOfのなかにじゃんじゃんDaoを追加していくことでこのDatabaseを呼び出します。
更新があるたびにversionを上げていくことになります。また、その他のDaoが増えたときにはここに追加していくことになります。

Database呼び出し

class SampleApplication : Application() {
    companion object {
        lateinit var database: DataBase
    }

    override fun onCreate() {
        super.onCreate()

        database = Room.databaseBuilder(this, objectOf<DataBase>(), "kotlin_room_sample.db").build()
    }
}

internal inline fun <reified T : Any> objectOf() = T::class.java

サンプルなのでApplictionに置いてありますが、ホントはDagger2等で注入してあげたほうがテストは書きやすいかと思います。

データの追加、呼び出し、削除

さて、ApplicationにDatabaseを置いたので呼び出してみましょう。

create

        val dao = SampleApplication.database.addressDao()
        async(UI) {
            bg {
                dao.createAddress(address)
            }
        }

とはいえとても簡単です。
先ほどシングルトン代わりに置いたApplicationをdatabaseから持ってきたdaoを呼び出します。
で、まぁ一応RoomはUIスレッドでも実行することができるんですが、普通に考えてバックグラウンドで処理するべきなのでバックグラウンドでcreateします
ちなみにUIスレッドで使いたい場合はdatabaseに対して

.allowMainThreadQueries()

することでイケます。

query

    private fun loadAddress() {
        async(UI) {
            val address: Deferred<List<Address>> = bg { getAddress() }
            adapter.addItems(address.await())
            adapter.notifyDataSetChanged()
            resetEmptyView()
        }
    }

    private fun getAddress(): List<Address> {
        val dao = SampleApplication.database.addressDao()
        return dao.findAll()
    }

次にQueryメソッドを呼び出してみます。
ここではList用に保存されたすべてのデータを持ち出します。
kotlinだとasync,awaitが使えるので効果的に記述することができます。

Roomからの取得を待ってListにデータの表示を反映させる、という具合ですね。

delete

    private fun deleteAddress(address: Address) {
        async(UI) {
            bg {
                val dao = SampleApplication.database.addressDao()
                dao.delete(address)
            }
           // UI操作
        }
    }

次にdeleteをしてみます。
こちらもbgでdeleteします。ただし、UI操作はawaitしません。UI操作を待ってしまうと、ユーザーにはよくわからない時間が発生してしまうためです。

以上です。

終わりに

以上となります。Roomを書く上で恩恵を受けられる部分は、Data object と async,await あとはスレッド管理がkotlinだと簡単に書けますね。