本記事ではAACのRoomPersistenceLibraryとRxJavaを組み合わせた、基本的な実装のお話をしていこうと思います。
記事内のコードはKotlinで記述していますが、Coroutineなどは使っていないため、基本的にJavaに置き換えることも可能です。
対象読者
- Room初学者
- Room + RxJavaに興味がある方
セットアップ
最初にbuild.gradle(app)に以下を記述します。
def room_version='2.1.0-alpha06'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
-
androidx.room:room-rxjava2
を記述することによって、DAOの戻り値でSingleやMaybeなどを指定することができます。 -
room_version
に関しては2.1.0
以上を使ってください。理由は後ほど詳しく記述しますが、2.1.0からInsertやdeleteの戻り値にCompletableを指定できるようになったからです。 - AndroidXに対応していないと、
2.0.0
以上のバージョンを使うことができないため、1.1.1
を使ってください。詳しくは こちら のサイトが参考になると思います。
Entity
次にEntityを記述します。
@Entity(tableName = "user")
data class UserEntity constructor(
@PrimaryKey(autoGenerate = true) //default false
val id: Long = 0,
@ColumnInfo(name = "user_name")
val userName: String,
val createAt: String
)
-
@Entity
これをつけることによって、Roomで利用できるEntityになります。tableName
はオプションで、クラス名と実際に生成されるTable名を変えたいときにつけます。 -
@PrimaryKey
は、1つのEntityに最低1つはつける必要があります。複数のPrimaryKeyを持つ場合は、以下のように@Entity
アノテーション内に定義できます。
@Entity(primaryKeys = ["id", "user_name"])
-
autoGenerate = true
になっている場合、idに0
を渡すと、自動的にincrementしてくれます。idを指定する必要がない場合は、0
を指定してください。 -
@Ignore
を定義することによって、そのカラムを無視することができます。しかしdata classで@Ignore
を定義するとEntities and Pojos must have a usable public constructor.
というエラーが吐かれます。これは全てのカラムにデフォルト値を指定することによって解消できます。
@ColumnInfo(name = "user_name")
var userName: String = "",
@Ignore
var createAt: String = ""
DAO
次にData Access Object(DAO)を記述します。
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertiOrUpdateUser(entity: UserEntity): Completable
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertiOrUpdateUsers(entities: List<UserEntity>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Single<UserEntity>
@Delete
fun delete(entity: UserEntity): Completable
}
-
@Insert
は、UserEntity単体、複数(List)のどちらでもInserすることができます。また、onConflict
は、任意の値を指定することによって、PrimaryKeyが同じものがInsertされた場合の挙動を指定することができます。今回は上書きするようにOnConflictStrategy.REPLACE
を指定しました。その他の指定については こちら のサイトが参考になると思います。 -
@Insert
の戻り値はCompletableを指定できます。 -
@Delete
では引数にUserEntityを渡すことによって、渡したEntityを削除することができます。この戻り値もCompletableを指定することができます。
Query
@Query
の戻り値にはMaybe, Single, Flowable/Observableを指定することができます。各々挙動が違うので、要件に合ったものを選ぶといいと思います。
Maybe
@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Maybe<UserEntity>
- データベース内に該当するUserEntityが
ない
場合、何も返さずにonComplete
が呼ばれます。 - データベース内に該当するUserEntityが
ある
場合、onSuccess
が呼ばれ、終了します(onCompleteは呼ばれません)。 - Maybeが終了した後にUserEntityが更新されても何もしません。
Single
@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Single<UserEntity>
- データベース内に該当するUserEntityが
ない
場合、onError(EmptyResultSetException)
が呼ばれます。- データベース内に該当するUserEntityが
ない
かつ、List型を返す場合はonError(EmptyResultSetException)
が呼ばれず、空のListが返されます。
- データベース内に該当するUserEntityが
- データベース内に該当するUserEntityが
ある
場合、onSuccess
が呼ばれ、終了します。 - Singleが終了した後にUserEntityが更新されても何もしません。
Flowable/Observable
@Query("SELECT * FROM user WHERE id = :id")
fun findById(id: Long): Flowable<UserEntity>
- データベース内に該当するUserEntityが
ない
場合、何も返さず、何も呼ばれません。 - データベース内に該当するUserEntityが
ある
場合、onNext
が呼ばれます。 - UserEntityが更新されるたびに、自動で
onNext
が呼ばれます。
Database
次にDatabaseを記述します。
@Database(entities = [UserEntity::class], version = SampleDatabase.DATABASE_VERSION)
abstract class SampleDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
const val DATABASE_VERSION = 1
private const val DATABASE_NAME = "sample.db"
private var instance: SampleDatabase? = null
fun init(context: Context) {
Room.databaseBuilder(context, SampleDatabase::class.java, DATABASE_NAME)
.build().also { instance = it }
}
fun getInstance() = instance
}
}
- RoomDatabaseを継承したabstractクラスを作成します。
-
@Database
内のentities
にアプリ内で使うEntityを宣言します。Entityが増えるたびに、ここに追加していく形になります。 -
@Database
内のversion
にデータベースのversionを定義します。最小値は1です。 -
abstract fun userDao(): UserDao
の部分はDAOの宣言になります。DAOが増えるたびに、ここに追加していくことになります。 - init()やgetInstance()を生やしておき、外部から呼び出せるようにしておきます。
Databaseの初期化
最後に上で定義したDatabaseをApplicationクラスで初期化します。
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
SampleDatabase.init(this)
}
}
簡単ですね。これでSampleDatabaseをどこからでも呼び出せるようになりました。
使ってみる
それでは準備が整ったので、実際に使ってみようと思います。
今回は
- 複数のUserEntityをInsert
- idからUserEntityを取得
- 2で取得したUserEntityを削除
この3つの処理を書いていこうと思います。
val users: List<UserEntity> = //任意の値を入れてください
SampleDatabase.getInstance()?.let { sampleDatabase ->
val userDao = sampleDatabase.userDao()
userDao
.insertiOrUpdateUsers(users)
.andThen(userDao.findById(1)) //Single<UserEntity>を返しています
.flatMapCompletable { user -> userDao.delete(user) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, { e -> e.printStackTrace() })
}
たったこれだけです! すごくシンプルに書けます。
-
subscribeOn()
ここだけ説明しておきます。Room実行は基本的にBackgroundThreadを指定しないといけません。MainThreadで実行するとExceptionが吐かれます。もしMainThreadでの実行を許可したい場合は、RoomのInstance生成時に以下のような定義が必要になります。
Room.databaseBuilder(context, SampleDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries() //MainThreadでの実行を許可します
.build().also { instance = it }
最後に
RoomはThread制限があったり、Queryが間違っていたらBuild時に教えてくれたり、とても親切なライブラリです。
皆さんも是非使ってみてください。