概要
JetpackにSQLiteを使いやすくするためのライブラリである「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に置き換えることのデメリットはあまりないような気がします。