【Kotlin/Android Studio】RoomDataBaseの使い方!Daoとは?
この記事からわかること
- Android Studio/KotlinでRoomデータベースの使い方
- ローカルにデータを永続的に保存する方法
- アプリを停止してもデータを保持するには?
- エンティティやDAOとは?
- アノテーションの種類
- 実際アプリで実装できるサンプルコード
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
参考文献:公式リファレンス:Roomを使用してローカルデータベースにデータを保存する
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
Roomとは?
Androidで使用するRoomとはアプリ内からローカルにデータを永続的に保存できるJetPackに含まれたライブラリの1つです。Roomは「SQLite」という既存のRDBMS(Relational DataBase Management System)に対して抽象化したレイヤを提供することでAndroidからデータベースへのスムーズなアクセスを可能にしています。AndroidからではSQLite APIを使用して普通に操作することも可能ですが、SQLiteを最大限に活用できるRoomを使用することが公式より推奨されています。
またRoomではCRUD処理を非同期処理で実装することがルールとなっています。実際にアプリ内で動かすためには少しクセがあるので注意してください。小さいデータであればSharedPreferencesやDataStoreを使用する方が簡単にデータを操作できるので参考にしてください。
Roomを使用するにあたって重要になるクラス
RoomでDB操作を実装するためには以下のクラスが必要になってきます。少しややこしいですがそれぞれの役割をしっかり把握しながら実装していきたいと思います。
- データベースクラス・・・データベース本体を保持するクラス
- エンティティ(モデル)クラス・・・格納するデータのテーブル
- DAO(データアクセスオブジェクト)・・・データベースのデータを操作するメソッドを提供
DAO(ダオ or ディーエーオー)とはその名の通りデータベースにアクセスしデータを取得、更新、削除などの機能を持ったオブジェクトのことです。DBからデータを取得するといえばSQLを思い浮かべますが、まさしくSQLが絡んでおり、このDAOで定義するメソッドはSQLクエリに自動的にマッピングされます。
Roomの実装の流れ
- Roomライブラリの導入
- エンティティの定義
- DAOを定義する
- データベースの定義
今回のプロジェクトの全体はGitHubに上げていますので参考にしてください。
1:Roomライブラリの導入
Roomを使用するためにはAndroid Studioの中にRoomライブラリを導入する必要があります。まずは「bundle.gradle(Module)」に以下のコードを記述します。追加する箇所が多いので注意してください。
plugins {
// 〜〜〜〜〜〜〜
id 'kotlin-kapt'
}
android {
// 〜〜〜〜〜〜〜
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
dependencies {
// 〜〜〜〜〜〜〜
def room_version = "2.5.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'androidx.room:room-rxjava2:2.3.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
}
追加した箇所と項目
- plugins内:「id 'kotlin-kapt'」を追加
- android内:「 kotlinOptions { }」の周りを「tasks.with... { }」で囲む
- dependencies内:「def room_version = "2.5.0"」以降全て
kotlin-kapt
は「kotlin-annotation-processing tools」の略で、JavaのPluggable Annotation Processing APIをKotlinで使えるようにするためのプラグインです。
Roomのバージョンは現在(2023年7月時点)のものなので適切なバージョンに変更してください。また導入しているライブラリのうち上3つはRoomに関するもの下の4つはRxJavaに関するものです。Roomを扱う上であると便利なので一緒に導入しておきます。
おすすめ記事:【Android Studio】RxJavaの使い方と導入方法!Observableオブジェクト
記述できたら「Sync Now」をクリックします。私は「tasks.with... { }」がなかった(公式には載っていなかった)ため以下のようなエラーが発生しました。
2:エンティティの定義
データベースに保存するデータの構造(テーブル)部分であるエンティティクラスを定義します。今回はUser
クラスをroom
ディレクトリを作成してapp/java/com.example.room/room/
内に定義しておきました。
@Entity(tableName = "user_table")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val age: Int,
val hobby: String
)
ここで普通のデータクラスと違うのは@Entity
や@PrimaryKey
というアノテーションがついていることです。これを付与することでRoomにエンティティであることを認識させます。また引数にはテーブル名を渡します。引数を省略した場合はクラス名と同名のテーブルが自動生成されますが明示的に"user_table"
のような名前をつけておくことが公式より推奨されています。@PrimaryKey
は名前の通り主キーとして指定しています。
もしプロパティの名称とカラムに登録する名称を変更したい場合は@ColumnInfo
アノテーションを付与して引数に使用したい名称を渡します。SQLiteのテーブル名と列名では大文字と小文字が区別されないので注意してください。
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
その他のアノテーションや役割は以下の公式ページを参考にしてください。
公式リファレンス:Room エンティティを使用してデータを定義する
3:DAOを定義する
次にデータベース操作を行うためのメソッドを提供するDAOを定義していきます。getAll
メソッドの返り値で使用しているFlowable
がRxJavaになります。
おすすめ記事:【Kotlin/Android】RxJavaのFlowableの使い方!Observableとの違い
@Dao
interface UserDao {
@Insert
fun insert(user: User)
@Query("SELECT * FROM user_table")
fun getAll(): Flowable<List<User>>
@Query("SELECT * FROM user_table WHERE id = :id")
fun getLiveDataUser(id: Int): LiveData<User>
@Query("DELETE FROM user_table")
fun deleteAll()
}
ここでもアノテーションを多く使用していることがわかります。@Dao
でDAOであることを@Insert
などデータベースへのCRUD処理を定義しています。また@Query
アノテーションを使用することでSQL文をそのまま使用したメソッドも実装することが可能です。他にも様々なアノテーションが用意されているので公式ページを参考にしてください。
公式リファレンス:Room DAO を使用してデータにアクセスする
4.データベースの定義
続いてここまでで作成したエンティティとDAOを使用するためのデータベース本体を定義していきます。ここではやることが多いので先にまとめておきます。
データベースの定義のルール
- @Databaseアノテーションを付与(引数でDBバージョンを指定)
- RoomDatabaseを継承した抽象クラス(abstract class)として作成する
- Daoを返す抽象メソッドの定義
- シングルトンパターン
- データベースが存在しない場合のみ作成し、それ以外は既存のデータベースを返す
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: UserDatabase? = null
fun getDatabase(context: Context): UserDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
4.1:@Database(entities,version,exportSchema)
やっていることが多いので1つずつみていきます。まずは@Database
アノテーションでデータベースであることを認識させ、引数にエンティティとデータベースのバージョン(マイグレーション時に必要)、バックアップの有無を指定します。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() { ... }
ルール通り抽象メソッドからDaoを取得できるようにしておきます。
abstract fun userDao(): UserDao
4.2:シングルトンの実装:companion object
companion object
はインスタンス化しなくてもアクセスできるものでした。シングルトンを実装するためにその中に自身をインスタンス化して取得するメソッドを定義しています。
companion object { ... }
@Volatile
@Volatile
をプロパティに付与することでその変数はキャッシュされず読み書きはメインメモリで行われるようになります。つまりINSTANCE
プロパティはDBへの読み書きが常に反映されている最新のDBインスタンスを保持できることになります。ちなみにVolatileは「揮発性」という意味の英単語です。
@Volatile
private var INSTANCE: UserDatabase? = null
4.3:synchronizedメソッド
ここでは定義したINSTANCE
プロパティの値がnullの場合のみsynchronizedメソッドが実行されるようにエルビス演算子?;
が使用されています。synchronized
メソッドは複数のスレッドが同時にデータベースのインスタンスを作成しないようにするための同期ブロックです。このブロック内の処理は1つのスレッドしか同時に実行されません。
fun getDatabase(context: Context): UserDatabase {
return INSTANCE ?: synchronized(this) { ... }
}
4.4:Room.databaseBuilder
Room.databaseBuilder
でRoomのビルダーオブジェクト(オブジェクトのインスタンスを作成するためのパターンの1つ)を生成しています。
・context.applicationContext
・・・アプリケーションのコンテキストを取得
・UserDatabase::class.java
・・・Roomデータベースのデータアクセスクラスを指定
・"user_database"
・・・データベースクラスの名前を指定
val instance = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user_database"
)
.fallbackToDestructiveMigration()
.build()
fallbackToDestructiveMigration
メソッドはマイグレーション時正常に移行できるようにbuild
メソッドで実際にインスタンスを生成しています。
5:実際にアプリ内から使用してみる
ここまで来たら実際にアプリ内からRoomを操作してみたいと思います。なんとか1画面に収めたかったのでUserの名前のみ登録できる仕様になってしまいましたが、Roomに登録されていくデータはリアルタイムでRecyclerViewに反映されるように実装してみました。
一応MainActivity.ktだけ載せておきます。これをコピペしただけでは動かないのでGutHubにコードの全体を上げているの参考にしてください。
実装する際の注意点としてデータベースへのCRUD処理などはメインスレッドでは行わすに非同期で実行する必要があります。今回はRxJava
のCompletable.fromAction
を使用して実装しましたが、Kotlin Coroutinesなどでも実装できると思います。
おすすめ記事:【Kotlin/Android】RxJavaのCompletableの使い方!完了の是非だけ通知
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.room.room.User
import com.example.room.room.UserDao
import com.example.room.room.UserDatabase
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
class MainActivity : AppCompatActivity() {
lateinit var db : UserDatabase
lateinit var dao : UserDao
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val display:TextView = findViewById(R.id.display_text)
val addButton:Button = findViewById(R.id.add_button)
val getButton:Button = findViewById(R.id.get_button)
val allDeleteButton:Button = findViewById(R.id.all_delete_button)
val input:EditText = findViewById(R.id.input_text)
db = UserDatabase.getDatabase(this)
dao = db.userDao()
addButton.setOnClickListener{
if (input.text.toString() != "") {
display.text = "入力したよ"
insUser(input.text.toString())
}else{
display.text = "未入力です"
}
}
getButton.setOnClickListener {
display.text = "取得したよ"
getUser(0) // 取得したいIDを渡す
}
allDeleteButton.setOnClickListener {
display.text = "リセットしました"
deleteAll()
}
subscribeAllUser()
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
private fun insUser(name: String) {
val user = User(0, name, 20, "サーフィン")
Completable.fromAction {
dao.insert(user)
}.subscribeOn(Schedulers.io())
.subscribe()
.addTo(compositeDisposable)
}
private fun getUser(id : Int){
val display: TextView = findViewById(R.id.display_text)
val userLiveData = dao.getLiveDataUser(id)
userLiveData.observe(this, { user ->
if (user != null) {
display.text = user.name
} else {
display.text = "User not found"
}
})
}
private fun deleteAll(){
Completable.fromAction { dao.deleteAll() }
.subscribeOn(Schedulers.io())
.subscribe()
.addTo(compositeDisposable)
}
private fun subscribeAllUser(){
val recyclerView: RecyclerView = findViewById(R.id.main_list)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
dao.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
//データ取得完了時の処理
val data = ArrayList<User>()
it.forEach {
user -> data.add(user)
}
val adapter = MainAdapter(data)
recyclerView.adapter = adapter
}
).addTo(compositeDisposable)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。