Android
Kotlin

ORMラッパーライブラリRoomをKotlinで使ってみる

More than 1 year has passed since last update.

最近公式のドキュメントに追加されたRoomライブラリを使うと、Android組み込みのSQLiteデータベースにアクセスする処理がとても簡単に記述できます。RoomライブラリはよくあるORMラッパーの一種ですが、アノテーション処理でクラスを生成するのでリフレクションを使わずパフォーマンスが良いことと、SQLの中に現れるカラム名のミスなどがコンパイル時にエラーとして検出できることが強みです。
インメモリデータベースとしても使用できるしLiveDataと一緒に使えばデータ変更を検知してUIを更新するような処理がとても簡単に記述できます。ServiceとActivity間のデータのやり取りなんかも楽ちんです。
私は長らくORMLiteを使っていましたが、今後のプロジェクトでは積極的にRoomを使っていこうと思います。

公式のドキュメントではJavaでの使い方しか書いていなかったので、Kotlinで使う方法を調べてみました。基本的にはJavaで書いているのと同等のクラスを生成するKotlinコードを書けば済むのですけどね。

プロジェクトにRoomを追加する

アプリモジュールのbuild.gradleファイル(通常はapp/build.gradle)を編集します。
やることはKotlinでアノテーション処理を行うためにkotlin-kaptプラグインを使うようにすることと、dependenciesブロックでRoomライブラリの宣言をすることです。

app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // これを追加

android {
    ...
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.android.support:recyclerview-v7:26.1.0'

    // ここの2行を追加 
    implementation "android.arch.persistence.room:runtime:1.0.0"
    kapt "android.arch.persistence.room:compiler:1.0.0"
}

これでAndroid StudioのSync Project with Gradle FilesをすればRoomライブラリが使用できるようになります。

データモデルを作成する

RoomのようなORMライブラリではテーブルに保存される各行のデータをクラスとして表現します。必要なデータ構造をクラスとして定義することで、そのデータを保存するのに必要なテーブルが自動的に生成されます。
今回は簡単に、IDと名前というデータを持つUserというオブジェクトを考えます。

ここに普通のクラスがあるじゃろ?

User.kt
class User {
    var uid: Int = 0
    var firstName: String? = null
    var lastName: String? = null
    var age: Int = 0
}

これを…こうじゃ!

User.kt
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey

@Entity
class User {
    @PrimaryKey
    var uid: Int = 0

    @ColumnInfo(name = "first_name")
    var firstName: String? = null

    @ColumnInfo(name = "last_name")
    var lastName: String? = null

    var age: Int = 0
}

まず、クラス自体に@Entityアノテーションを付けます。それから主キーのプロパティに@PrimaryKeyを付けます。@PrimaryKeyを付けたプロパティはNon-nullでなくてはいけませんので、注意してください。たとえばString?にしているとエラーになるので、便宜上適当な初期値を設定しておく必要があります(""など)。
その他のプロパティには何も付けなくても良いですが、@ColumnInfoアノテーションを付けることでカラムの詳細な設定ができます。ここで指定しているname属性は後でSQLを書く時のカラム名として使います。デフォルトではカラム名はプロパティ名と同じになります。

DAOを作成する

データベースアクセスをする処理を作成する場合、DAO(Data Access Object)というのを使うのが一般的です。簡単に言うとDAOというクラスを作ってそのクラスにデータベースアクセスを行う処理を書いておき、他のクラスからのデータを読み書きはDAOのメソッド経由で行う、という実装パターンです。こうしておくと何が良いかというと、DAOを利用する側からはそのデータがどこにどのように保存されるのか、どこから読み出されるのかを気にしなくてよくなるという利点があります。
実際にデータベースを読むのではなくキャッシュから読み込んだり、データベースのバックアップが複数存在したり、といった裏側の細かいことはDAOだけが知っていればよくて、データを利用するアプリケーション側は知る必要はありません。

Roomでは、このようなデータベースアクセスを行うDAOをなんとアノテーション処理で自動生成してくれます!必要なのはどういうインターフェースでデータを読み書きする必要があるか?というのをアノテーションで指示してあげるだけです。

それでは、上記のUserを処理するDAOを作成するにはどのようにすれば良いのかを見てみましょう。

UserDao.kt
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query

@Dao
interface UserDao {

    // シンプルなSELECTクエリ
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    // メソッドの引数をSQLのパラメーターにマッピングするには :引数名 と書く
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllaByIds(vararg userIds: Int): List<User>

    // 複数の引数も渡せる
    @Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User

    // データモデルのクラスを引数に渡すことで、データの作成ができる。
    @Insert
    fun insert(user: User)

    // 可変長引数にしたり
    @Insert
    fun insertAll(vararg users: User)

    // Listで渡したりもできる
    @Insert
    fun insertAll(users: List<User>)

    // データモデルのクラスを引数に渡すことで、データの削除ができる。主キーでデータを検索して削除する場合。
    @Delete
    fun delete(user: User)

    // 複雑な条件で削除したい場合は、@Queryを使ってSQLを書く
    @Query("DELETE FROM user WHERE age < :age")
    fun deleteYoungerThan(age: Int)
}

@Queryのアノテーションには実行するSQLを記述します。:first等の:で始まる名前はメソッドの引数の値に置き換えられます。

データベースクラスを作成する

最後にお約束的に必要なクラスがあります。下記のようなクラスを作成してください。

AppDatabase.kt
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase

@Database(entities = arrayOf(User::class), version = 1) // Kotlin 1.2からは arrayOf(User::class)の代わりに[User::class]と書ける
abstract class AppDatabase : RoomDatabase() {

    // DAOを取得する。
    abstract fun userDao(): UserDao

    // valでも良い。
    // abstract val dao: UserDao
}

abstractとして宣言されていることに注意してください。このクラスの実装もRoomライブラリのアノテーション処理により自動生成されます。

データの保存

さあここまで準備すればRoomライブラリを使ってデータ(User)の読み書きができるようになります。
データを保存するには以下のような処理を書きます。

// 永続データベースを作成
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name").build()

// もしくは
// インメモリデータベースを作成
// val db = Room.inMemoryDatabaseBuilder(applicationContext, AppDatabase::class.java).build()

// データモデルを作成
val user = User()
user.uid = Random().nextInt()
user.firstName = "Yuya"
user.lastName = "Matsuo"

// データを保存
db.userDao().insert(user)

// データを削除
db.userDao().delete(user)

まず最初にデータベース(AppDatabase)のインスタンスを作成します。このインスタンス作成処理には時間がかかるので、アプリケーションの開始時(もしくはデータベースが最初に必要になる時)に一度だけ行い、以後はインスタンスを使いまわすことが推奨されています。一方、db.userDao()でDAOを取得しますが、こちらのDAOのインスタンスはAppDatabase内部でキャッシュされるため、このメソッド呼び出しは何回行っても大丈夫です。

説明のためにコードを簡略化しましたが、上記のinsertAll()メソッドはAndroidのメインスレッドから呼び出すとエラーで落ちるということに注意してください。データベースの処理は場合によってとても時間がかかることがあるため、メインスレッドから呼び出すとUIが固まってしまうことがあります。そういうことが起こらないようにするため、メインスレッドからは呼び出すことができないようになっています。
なお、この制限はデータベース作成時にallowMainThreadQueries()を呼ぶことで無効化することができますが、メインスレッドでブロックされる可能性のある処理を行うことは推奨されていないのでちょっと開発時に使いたい場合のみ使用するようにしてください。

Kotlinではthread {}で手軽にスレッドを作成して処理することができます。とりあえず難しいことを考えずにバックグラウンドスレッドでデータベース処理を行うには、以下のようにします。

val user = User()
user.uid = Random().nextInt()
user.firstName = "eje"
user.lastName = "taro"

thread {
    db.userDao().insertAll(user)
}

スレッドに関してより細かな制御をしたい場合はExecutorServiceを使うと良いでしょう。

データの取り出し

全てのUserを一覧取得するにはdb.userDao().getAll()を呼べば良いです。IDや名前で検索する場合はloadAllaByIdsfindByNameを使います。

LiveDataとの連携

LiveDataとの連携ができることがRoomの最も素晴らしい機能と言っても良いでしょう。LiveDataを使うことで、バックグラウンドでデータの更新を行いUIの表示をリアルタイムに更新するような処理がとても簡単に作れます。

使い方はとても簡単で、DAOにLiveDataを返すメソッドを定義するだけです。先程のUserDaoの例だと、このようになります。

UserDao.kt
@Dao
interface UserDao {

    @Query("SELECT * FROM user")
    fun getAll(): LiveData<List<User>>

}

そしてユーザー一覧を表示したいUI側(ActivityやFragmentなど)では以下のようにしてデータの取得とUIへの反映を行います。

// this はsupport v4のFragmentもしくはAppCompatActivityを継承したクラス
db.userDao().getAll().observe(this, Observer<List<User>> {users ->
    // ユーザー一覧を取得した時やデータが変更された時に呼ばれる
    if (users != null) {
        // TODO ユーザー一覧をRecyclerViewなどで表示
    }
})

LiveDataに関するより詳しい使い方はドキュメントを読んでください。