【Kotlin/Android Studio】RoomDataBaseの使い方!Daoとは?

この記事からわかること

  • Android Studio/KotlinRoomデータベース使い方
  • ローカルデータ永続的保存する方法
  • アプリ停止してもデータを保持するには?
  • エンティティDAOとは?
  • アノテーション種類
  • 実際アプリで実装できるサンプルコード

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献:公式リファレンス:Roomを使用してローカルデータベースにデータを保存する

環境

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(ダオ or ディーエーオー)とはその名の通りデータベースにアクセスしデータを取得、更新、削除などの機能を持ったオブジェクトのことです。DBからデータを取得するといえばSQLを思い浮かべますが、まさしくSQLが絡んでおり、このDAOで定義するメソッドはSQLクエリに自動的にマッピングされます。

Roomの実装の流れ

今回のプロジェクトの全体は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'

}

追加した箇所と項目

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(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に反映されるように実装してみました。

【Kotlin/Android Studio】RoomDataBaseの使い方!Daoとは?

一応MainActivity.ktだけ載せておきます。これをコピペしただけでは動かないのでGutHubにコードの全体を上げているの参考にしてください。

実装する際の注意点としてデータベースへのCRUD処理などはメインスレッドでは行わすに非同期で実行する必要があります。今回はRxJavaCompletable.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)
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index