It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【Android】【Jetpack】Roomの使いかた

概要

JetpackSQLiteを使いやすくするためのライブラリである「Room」というものがあります。
モバイルのデータベースといえば、昔から主流はSQLiteでしたが個人的には最近はRealmを使うことが多くSQLiteはしばらく触ってませんでした。
(SQLiteは、使うまでの手続きがどうしても面倒で...)
しかし、Roomを使うとそのへんが簡単になりそうとのことなので、一度試してみようとおもいます。

環境

  • kotlin: 1.3.72
  • AndroidStudio: 4.0
  • compileSdkVersion: 29

環境のセットアップ

まずは app/build.gradle に依存モジュールを追加します。
現時点での最新の安定版は「2.2.5」です。
Roomでは、アノテーションプロセッシングを使うので、Kotlinで実装する場合は、kotlin-kaptが必要です。

apply plugin: 'kotlin-kapt'
・・・
dependencies {
・・・
    def room_version = "2.2.5"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
・・・
} 

次にデータベーススキーマの情報をjsonファイルとして書き出す設定です。

    defaultConfig {
・・・
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                        "room.schemaLocation":"$projectDir/schemas".toString(),
                        "room.incremental":"true",
                        "room.expandProjection":"true"]
            }
        }
    }

これを記載することで、各バージョン時点でのスキーマファイルがプロジェクト配下の schemas ディレクトリへ作成されます。
今回の対象ではありませんが、スキーマファイルを使うことでマイグレーションテストを書くことができるようになるなどメリットがあるため設定しておくのが良いかと思います。

Roomの構成概要

Roomは主に3つの役割のコンポーネントで構成されます。

  • エンティティ: データベース内のテーブルを表現する。カラムとプロパティの紐付け。
  • DAO: データベースにアクセスしてEntityを操作する。CRADをここに記載するイメージ。
  • データベース: データベースへの接続を管理する。

これらのコンポーネントを実装していきます。

エンティティの作成

まずはテーブルを表現するための、エンティティクラスを作成します。

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

// テーブルには@Entityをつける
@Entity(tableName = "users")
data class User(
    // 主キーには @PrimaryKey をつける
    @PrimaryKey var id: String,
    // カラムには @ColumnInfo をつける
    @ColumnInfo val name: String,
    // 実カラム名との関連はname引数で渡す
    @ColumnInfo(name = "phone_number") val phoneNumber: String
)
  • エンティティには @Entity というアノテーションをつけます
    • DBの実テーブル名との関連を表現することもできます
  • idは主キーとして扱いたいため、@PrimaryKey アノテーションをつけます
  • その他カラムには @ColumnInfoとつけます
    • DBの実カラム名との関連を表現することもできます
  • その他にも外部キーを表現するための @ForeignKey なども使えます

DAOの作成

次にDAOを作成します。
DAOは、データベースを操作するメソッドを記述した interface として作成します。

import androidx.room.*

// DAOには @Dao をつける
@Dao
interface UserDao {
    // @Queryを使うと実行SQLを定義することができる
    @Query("SELECT * from users")
    fun getAllUsers(): List<User>

    // SQLに引数を渡す場合は 「:<引数名>」 で関連付ける
    @Query("SELECT * from users where id = :id LIMIT 1")
    fun findById(id: String): User

    // @Insertなど用意されたアノテーションをつけると引数にEntityを渡すだけで処理ができる
    @Insert
    fun insert(user: User)

    @Update
    fun update(user: User)

    @Delete
    fun delete(user: User)
}
  • DAOには @Dao というアノテーションをつけます
  • @Query アノテーションをつかうことで、SQLを直接記述できます
  • @Insert, @Update, @Delete をつかうと引数にエンティティを渡すだけで処理できます

データベースの作成

最後にデータベースを作成します。

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

// entities には、関連付けるテーブルのエンティティを渡す
@Database(entities = [User::class], version = 1)
abstract class SampleDatabase: RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        // シングルトンで使えるようにする
        @Volatile
        private var instance: SampleDatabase? = null

        // データベースインスタンスは、マルチスレッドで参照される可能性があるため synchronized でアクセス
        fun getInstance(context: Context): SampleDatabase =
            instance ?: synchronized(this) {
                // データベースインスタンスは、databaseBuilderを使って作成
                Room.databaseBuilder(context, SampleDatabase::class.java, "thchbooster.db").build()
            }
    }
}

データベースのインスタンスは、 Room.databaseBuilder を使って作成します。

注: シングル プロセスで実行するアプリの場合は、AppDatabase オブジェクトをインスタンス化する際にシングルトン設計パターンに従ってください。各 RoomDatabase インスタンスは非常に高コストであり、単一のプロセス内で複数のインスタンスにアクセスする必要はほとんどありません。

公式のドキュメントにある通り、データベースインスタンスは毎回生成するのではなくシングルトンインスタンスとして保持します。

また、データベースのインスタンスに、各DAOオブジェクトへの参照を abstruct メソッドとして保持することでDAOへ参照できるようになります。

DB操作のサンプル

これらのRoomコンポーネントを使ってDBへアクセスしてみます。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btn_add_user).setOnClickListener {
            thread {
                val db = SampleDatabase.getInstance(this.application)
                val user = User("1", "bob", "090xxxxxxxx")
                db.userDao().insert(user)
            }
        }

        findViewById<Button>(R.id.btn_get_user).setOnClickListener {
            thread {
                val db = SampleDatabase.getInstance(this.application)
                val user = db.userDao().findById("1")
                println(user)
            }
        }

    }
}

DAO経由で insert 処理と findByIdによるセレクトの処理を記載しました。
注意すべきなのは、データベースへのアクセス(DAOのメソッド実行)は、メインスレッドで行うことができないためスレッドを分ける必要があります。

感想

SQLiteをイチから実装するよりも随分スッキリしていていい感じな気がしています。
realmのようなORMと比べると若干手続きに手間がかかる気もしますが、生のSQLを記載できる点やスキーマファイルを吐き出せる点など柔軟な設計もできるのは良いかなと思います。
自前で書くと結構汚くなってしまいそうなコンポーネントごとのレイヤーもある程度強制的に分離してくれるのは嬉しい点です。
既存でSQLiteを使っているならRoomに置き換えることのデメリットはあまりないような気がします。

参考