AndroidでRoomを使ってデータベースを実現
Androidでは色々なデータの管理方法があります。単純なファイル、key-valueストア、等など。ちょっと複雑な処理になると単純なテキストファイル、key-valueストアではすぐ限界になってしまいます。
Androidでは以前からRDBとしてSQLiteが使用可能でしたが、roomは更にそれらを抽象化して使いやすくしています。ORマッパーと同じ概念です。
公式ホームページでは
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、データベースへのスムーズなアクセスを可能にし、SQLite を最大限に活用できるようにします。特に、Room には次のようなメリットがあります。
・SQL クエリのコンパイル時検証。
・繰り返しが多く間違いを犯しやすいボイラープレート コードを最小限に抑える便利なアノテーション。
・効率的なデータベース移行パス。こうしたことから、SQLite API を直接使用するのではなく、Room を使用することを強くおすすめします。
と書かれています。Javaでサーバサイドの開発でORマッパーを使ってRDBにアクセスするのと同じような感覚でコーディングができます。
RoomはDBにアクセスする際に非同期処理を求められます。GUIと同じmainスレッドで実行すると例外がthrowされます。Androidの非同期処理の仕組みとして、AsyncTaskはAPI 30から非推奨となっているので、今の所以下の2通りの非同期処理の仕組みがあります。
- RxJava(Java、kotlin)
- coroutine(kotlin)
coroutineはkotlinに依存しているのでjavaでは使えません。kotlinだとどちらでも使えます。今回はkotlinですが、RxJavaを使ってみました。kotlinだとcoroutineの方が王道のようですが・・・今回はJavaでもkotlinでも使えるRxJavaにしてみました。
参考url
公式Android developers Room を使用してローカル データベースにデータを保存する
CodeZine リアクティブプログラミングとRxJavaの概要
build.gradleライブラリの依存関係
まず、ライブラリの依存関係からです。Room、RxJavaを使うと結構、色々なライブラリの依存関係が出てきます。
(Roomのバージョンは2022/11/9にバージョンが1個上がって、2.4.3が最新になっていますね)
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.room:room-rxjava3:$room_version"
implementation "androidx.room:room-guava:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.paging:paging-compose:1.0.0-alpha15'
implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
}
あと、RoomはDBのスキーマをテキストで都度出力することができるので、
defaultConfig {
(中略)
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
}
を追加しました。
まずは、DB設計から
SQLiteにどんなテーブルを持つか、DB設計から始めましょう。テーブルは2つでこんな画面を作ってみたいと思います。テーブル2つのそれぞれのEditTextに値を入れて追加ボタンを押すとテーブルにINSERT。上のTextViewに両方のテーブルを結合した結果がSELECTされて表示される。といった簡単な画面です。
テーブルは2つ。
ユーザ(User)
カラム | 名称 | 型 |
---|---|---|
uid | ユーザID | Int型、PKey、連番 |
first_name | 名 | String |
last_name | 姓 | String |
dept_cd | 所属コード | String |
所属(Dept)
カラム | 名称 | 型 |
---|---|---|
dept_cd | 所属コード | String、PKey |
dept_name | 所属名 | String |
上側のTextViewに表示するSELECT用のふたつのテーブルをリレーションで繋げたviewを作ります。
ユーザView(User_View)
カラム | 名称 | 型 |
---|---|---|
uid | ユーザID | Int型、PKey、連番 |
first_name | 名 | String |
last_name | 姓 | String |
dept_name | 所属名 | String |
まず、上記の2つのテーブルのEntityとViewのEntityが1個の合計3つのEntityを作ります。
User.kt
@Entity(tableName = "User")
data class User(
/** PKey */
@PrimaryKey(autoGenerate = true) val uid: Int,
/** ファーストネーム */
@ColumnInfo(name = "first_name") val firstName: String?,
/** ラストネーム */
@ColumnInfo(name = "last_name") val lastName: String?,
/** 所属コード */
@ColumnInfo(name = "dept_cd") val deptCd: String?
)
Dept.kt
@Entity(tableName = "Dept")
data class Dept(
/** PKey 所属コード */
@PrimaryKey @ColumnInfo(name = "dept_cd") val deptCd: String,
/** 所属名 */
@ColumnInfo(name = "dept_name") val deptName: String?
)
UserView.kt
@DatabaseView( viewName ="User_view",
value = """
select user.uid, user.first_name, user.last_name, dept.dept_name from user
left outer join dept on (user.dept_cd = dept.dept_cd)
""")
@Entity(tableName = "User_view")
data class UserView(
/** uid(PKey) */
@ColumnInfo(name = "uid") val uid: Int,
/** ファーストネーム */
@ColumnInfo(name = "first_name") val firstName: String?,
/** ラストネーム */
@ColumnInfo(name = "last_name") val lastName: String?,
/** 所属名 */
@ColumnInfo(name = "dept_name") val deptName: String?
)
RoomはこのEntityの定義を見て自動でDDLを生成し、SQLiteにCREATE TABLEをしてくれます。VIEWは@DatabaseViewアノテーションにViewの定義SQLを書くことでViewもCREATE VIEWしてくれます。
それぞれにDAOを作る
EntityのそれぞれにDAOを作ります。画面をみればだいたいの動きはわかりますが、今回はUser、DeptはINSERTしかありません。UserViewはViewなのでSELECTしかありません。が、今回はサンプルとしてUser、Deptは一般的に考えられるパターン、全部作ってみました。DAOはInterfaceで作ります。
CRUDで言うと、C(INSERT)、U(UPDATE)、D(DELETE)は特にSQLは書かなくてもアノテーションだけで実行するSQLを自動生成してくれます。(書くこともできる)
R(SELECT)はアノテーションの引数に実行するSQLを書きます。
UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): Flowable<List<User>>
@Query("SELECT * FROM user where uid = :uid")
fun getByPk(uid: Int): Single<User>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(users: User): Completable
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(usersList: List<User>): Completable
@Delete
fun delete(user: User): Completable
}
DeptDao.kt
@Dao
interface DeptDao {
@Query("SELECT * FROM dept")
fun getAll(): Flowable<List<Dept>>
@Query("SELECT * FROM dept where dept_cd = :deptCd")
fun getByPk(deptCd: String): Single<Dept>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(dept: Dept): Completable
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(deptList: List<Dept>): Completable
@Delete
fun delete(dept: Dept): Completable
}
UserViewDao.kt
@Dao
interface UserViewDao {
@Query("SELECT * FROM User_view")
fun getAll(): Flowable<List<UserView>>
}
DBクラスを作成する
Room(SQLite)のDB全体を表現する。DBクラスを作成します。ここはSingletonパターンで書きます。kotlinでSingletonパターンの書き方は色々あるようですが、とりあえず、一番わかりやすいパターンで書いてます。
@DatabaseアノテーションでDBとEntity、Viewの関連性が定義されています。このDBクラスはRoomDatabaseを継承し、抽象クラスとして作ります。
AppDatabase.kt
@Database(entities = [User::class, Dept::class], views = [UserView::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DB_NAME = "room-sample.db"
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context,
AppDatabase::class.java,
DB_NAME)
.build()
INSTANCE = instance
instance
}
}
}
// Dao
abstract fun userDao(): UserDao
abstract fun deptDao(): DeptDao
abstract fun userViewDao(): UserViewDao
}
MainActivityからRoomを呼ぶ処理
MainActivityからRoomを呼ぶ場合は上にも書いたとおり、非同期処理として呼び出さないと例外たthrowされて怒られます。今回は非同期処理はRxJavaを使って、RxJavaでDAOをラップするような感じで呼び出します。画面はRecyclerViewを使っていますが、その部分は覗いて、DBのアクセス部分だけ説明します。全体は完成版を参考にしてください。
DBの初期化とDAOのインスタンスの取得
DBの初期化はDBクラスのgetDatabaseメソッドを呼びます。DAOのインスタンスはそのDBクラスのインスタンスから取得します。
val db = AppDatabase.getDatabase(applicationContext)
val userDao = db.userDao()
val deptDao = db.deptDao()
val userViewDao = db.userViewDao()
UserにINSERT(非同期処理)
val disposable = userDao.insert(user)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ Log.d("User", "INSERT 成功")},
{ e -> Log.e("User", "INSERT 失敗", e) }
)
compositeDisposable.add(disposable)
DeptにINSERT(非同期処理)
val disposable = deptDao.insert(dept)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ Log.d("Dept", "INSERT 成功") },
{ e -> Log.e("Dept", "INSERT 失敗", e) }
)
compositeDisposable.add(disposable)
UserViewからSELECT(非同期処理)
val disposable = userViewDao.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
myAdapter.submitList(it)
Log.d("UserView", "SELECT ${it.size}件")
},
{ e -> Log.e("UserView", "SELECT 失敗", e) }
)
compositeDisposable.add(disposable)
UserとDeptはINSERTなので、INSERTは一般的に戻り値がありません。なのでINSERTが成功したか、失敗したかしかありません。subscribeの{}の1個めが成功のラムダ、または関数、2個めが失敗のラムダ、または関数です。
UserViewはSELECTで結果が返ってくるので、subscribeの{}の1個めの成功のラムダの中でRecyclerViewにSELECTした結果を渡して表示しています。
これは便利、App Inspection、sqlitebrowser
Roomを使ったアプリのデバック中にSQLiteの中身を見てみたい、更新してみたい場合があるとおもいます。
AndroidStudioのApp Inspectionを使用すると、動いているアプリのSQLiteの中身をリアルタイムで見ることができます。更新もできます。AndroidStudioのウィンドウの下側の枠の「App Inspection」をクリックします
すると、こんなビューが表示されます。
単純に中身を見るだけならテーブル名をクリックするとその右側のタブに表示されます。「Live Updates」のチェックをつけるとリアルタイムで更新された結果が表示されます。
SELECTでも条件で絞りたい場合、UPDATE、INSERT、DELETEはGUIだけではできないので、「Open New Query Tab」でタブを開いてSQLを自分で書く必要があります。
WindowsでSQLiteの中身を見たい場合はAndroidのDeviceFileExploerからSQLiteのファイルをWindowsにコピーします。SQLiteのファイルは
- /data/data/パッケージ名/databases
にできています。これをコピーして、sqlitebrowserで開くと見ることができます。
SQLのクエリを色々試してみたい場合だと、sqlitebrowserの方が使い勝手はいいですね。
以上、駆け足で説明しましたが、完成版はgitHubに置きました