アプリを開発していると、アプリやユーザーのデータを保存したいと思うことがあります。
入力されたデータを登録したり、更新したりしたいですよね。そんなときに使うのがデータベースです。
Androidには、優れたアプリを作るための「Android Jetpack」というライブラリがあります。
Android Jetpackライブラリの1つである「Room」について解説します。
Roomとは
Androidデベロッパーでは、Roomは次のように説明されています。
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、データベースへのより安定したアクセスを可能にし、SQLite を最大限に活用できるようにします。
Androidデベロッパー
Roomは、アプリのデータベース操作を簡単にしてくれるライブラリです。
データベースに保存するにはSQLiteを使う方法もありますが、Android デベロッパーではRoomを使うことを強くおすすめしています。
Androidデベロッパーで推奨しているアプリのアーキテクチャでは、図の左下がRoomの担当になります。
Roomを使うための4つのステップ
Roomは、テーブルの列を定義する「エンティティ」、データベースにアクセスするための「DAO」、これらをまとめた「データベース」から構成されます。
Roomを使うためには大きく4つのステップがあります。
- gradleに依存関係を追加する
- エンティティを作成する
- DAOを作成する
- データベースを作成する
今回は、ユーザーの情報を持ったテーブルを作成します。
id | name | age |
1 | Bob | 30 |
2 | Alice | 28 |
それでは1つずつ見ていきましょう。
ステップ1 gradleに依存関係を追加する
アプリでRoomを使用するには、次の3つの依存関係を追加します。
- room-runtime
- room-ktx
- room-compiler
「build.gradle(Module: app)」ファイルを開いて、次の依存関係を追加します。
plugins {
・・・
id 'kotlin-kapt' //kaptを使うために必要
}
dependencies {
・・・
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
room-ktx
room-ktxは、Android KTXの1つでKotlinの拡張機能です。
room-ktxを追加することで、データベーストランザクション向けのコルーチンが使えます。
データベースの操作は、実行に時間がかかる可能性があるため、コルーチンを使って非同期処理します。
room-compiler
Roomは、「@Entity」のような「@~」というアノテーションを使います。
アノテーションを使う場合、javaだと「annotationProcessor “androidx.room:room-compiler:$room_version”」と追加しますが、Kotllinだと「kapt “androidx.room:room-compiler:$room_version”」と追加します。
kapt(Kotlin annotation processing tool)を使うために、3行目でプラグインを適用しています。
plugins {
・・・
id 'kotlin-kapt' //kaptを使うために必要
}
次のようにプラグインを適用することもできます。
apply plugin: 'kotlin-kapt'
バージョンの定義
Roomのバージョンを定義します。
バージョンの定義は「build.gradle(Project: app)」で指定します。
「build.gradle(Project: app)」ファイルを開いて、room_versionを追加します。
ext {
・・・
room_version = "2.3.0"
}
最新のバージョンは、AndroidXのリリースページで確認することができます。
ステップ2 エンティティを作成する
Roomは、エンティティを使ってテーブルを作ります。
エンティティで定義されているフィールドが、テーブルの列になります。
今回、作るのは下表のようなテーブルです。
id | name | age |
1 | Bob | 30 |
2 | Alice | 26 |
テーブルには「id、name、age」の列があるので、エンティティで定義します。
ここで、nameのデータ型は文字列、ageは整数です。それぞれの型は、SQLとKotlinでは次のように異なります。
型 | SQL | Kotlin |
文字列 | TEXT | String |
整数 | INTEGER | Int |
例えば、文字列は、SQLではTEXL型、KotlinではString型になります。
しかし、Roomを使う場合は、Kotlinのデータ型を使用すれば、自動でSQLのデータ型にマッピングされます。
Kotlinクラスファイルで「User」を作成し、以下のコードを書きます。
@Entity(tableName = "user_table")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
@ColumnInfo(name = "name")
val name: String,
@ColumnInfo(name = "age")
val age: Int
)
@Entity
エンティティであることを示すために「@Entity」のアノテーションをつけます。
作られるテーブルは、クラスと同じ名前になりますが、「tebleName」プロパティを使うことで、テーブル名を指定できます。
@PrimaryKey
エンティティでは、少なくとも1つのフィールドを主キーとして定義する必要があります。
主キーは、テーブルのすべてのレコードを一意に識別するために使われます。
「@PrimaryKey」アノテーションを付けることで、そのフィールドが主キーになります。
「autoGenerate = true」を設定することで、自動でidが生成されます。これにより、idが一意になります。
@ColumnInfo
「@ColumnInfo」を使って、列の名前を設定します。
「@ColumnInfo」を使わないときは、変数名が列名になります。
ステップ3 DAOを作成する
DAOは、「データアクセスオブジェクト」のことで、アプリのデータにアクセスするために使います。
データベースにアクセスするためのメソッドを格納し、このメソッドはSQLクエリにマップされます。
つまり、KotlinコードでDAOのメソッドを呼び出せば、DAOで定義したSQLクエリでデータベースにアクセスします。
Kotlinクラスファイルで「UserDao」を作成し、以下のコードを書きます。
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM user_table")
suspend fun clear()
@Query("SELECT * FROM user_table WHERE id = :key")
suspend fun get(key: Long): User?
}
「@DAO」アノテーションをつけることで、DAOであることを示します。
DAOでは、一般的なデータベース操作で使う「@Insert」、「@Delete」、「@Update」アノテーションがあらかじめ用意されています。
- @Insert:行の挿入
- @Delete:行の削除
- @Update:行の更新
それ以外は、「@Query」アノテーションを使うことで、SQLiteでサポートされているクエリを作ることができます。
「@Query」の文字列パラメータにSQLクエリを指定することで、複雑な読み取りクエリなどが可能になります。
これは、user_tableのすべてのデータを削除するクエリです。
@Query("DELETE FROM user_table")
suspend clear()
SQLクエリでKotlin関数の引数を使うときは、コロン表記(:)を使います。
@Query("SELECT * FROM user_table WHERE id = :key")
suspend fun get(key: Long): User?
「”SELECT * from user_table WHERE id = :key”」のWHERE句のidは、「suspend fun get(key: Long): User?」の引数の「key」が参照されます。
suspend
データベースの操作は、実行に時間がかかる可能性があります。
メインスレッドで実行すると、データベースの操作が終わるまでほかの処理ができず、アプリの応答性が低下する恐れがあります。
そこでコルーチンの出番です。コルーチンを使用することで、メインスレッドをブロックせず、非同期で処理を行うことができます。
例えば、ピザを食べたいとき、同期処理だと自分でピザ屋まで取りに行きます。ピザを受け取り、自宅に帰るまではほかの作業は何もできません。
非同期処理だと、ピザの配達を頼み、ピザが配達されるまではほかの作業をすることができます。
簡単に言えば、コルーチンを使うことで、その処理をほかの人にお願いして、その間に別の処理をすることができるようになります。
しかし、通常の関数は、呼び出されるとその処理が終了するまで待ちが発生してしまいます。ピザの配達をお願いしたのに、配達されるまで何もせずに待っている状態です。
そこで登場するのが、suspendです。suspendを関数につけることで、その関数はメインスレッドをブロックしません。つまり、配達されるまでほかの処理をすることができるようになります。
DAOメソッドに「suspend」をつけることで、コルーチン機能を使ってDAOクエリを非同期にすることができます。
これで、データベースにアクセスするためのメソッドが定義できました。
DAOのメソッドを呼び出すことで、RoomがメソッドにマップされているSQLクエリを実行し、データベースにアクセスできます。
ステップ4 データベースを作成する
ステップ2とステップ3で作った、エンティティとDAOを使用する「データベース」を作成します。
データベースは、エンティティとDAOを定義する必要があり、データベースのインスタンスは1つのみです。
データベースは、複数のスレッドなどから同時にアクセスされる競合状態などは避ける必要があります。
以下は、データベースを作成するときのポイントです。
- @Databaseアノテーションをつける
- RoomDatabaseを継承した抽象クラスとして作成する
- DAOの抽象メソッドを定義する
- データベースは、シングルトンにして、アプリで1つのみ存在するようにする
- データベースが存在しない場合のみ作成し、それ以外は既存のデータベースを返す
実際に作成しましょう。 Kotlinクラスファイルで「UserRoomDatabase」を作成し、以下のコードを書きます。
このコードは、Roomを使うアプリのテンプレートとして使用できます。
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserRoomDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: UserRoomDatabase? = null
fun getDatabase(context: Context): UserRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
UserRoomDatabase::class.java,
"user_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
@Database
「@Database」アノテーションには、複数の引数が必要になります。
- entities:データベースに含まれるエンティティ
- version:データベースのバージョン
- exportSchema:スキーマのバージョン履歴のバックアップを保持するかどうか
@Database(entities = [User::class], version = 1, exportSchema = false)
「entities」は、ステップ2で作成した「User」を指定します。
「version」は、「1」を指定します。データベースのスキーマを変更するときは、バージョン番号を増やす必要があります。
スキーマは、データベースの構造のことです。
「exportSchema」は、データベースのスキーマを特定のフォルダにエクスポートするかどうかの設定になります。「false」を指定します。
DAOを宣言する
データベースはDAOについて知る必要があります。
そのため、クラス内でDAOの抽象メソッドを宣言します。
これによって、ほかのクラスがDAOクラスに簡単にアクセスできるようになります。
abstract fun userDao(): UserDao
companion object
「companion object」は、クラス内に作成されるシングルトンのことです。
companion objectを使うことで、クラスのインスタンスを生成せずにクラスのメソッドにアクセスすることができ、データベースが1つしか存在しないようにできます。
@Volatile
「@Volatile」アノテーションがついた変数は、キャッシュに保存されません。
これにより、読み書きはすべてメインメモリとの間で行われるため、変数の値が1つのスレッドで変更されると、ほかのスレッドでもすぐに反映されます。
INSTANCE変数に「@Volatile」アノテーションをつけることで、データベースのインスタンスが常に最新で、すべての実行スレッドで同じであることが確認できます。
@Volatile
private var INSTANCE: UserRoomDatabase? = null
INSTANCE変数は、最初にnullを格納するため、型に「?」をつけてnullを許容しています。
synchronized()
次のコードが、データベースのメインである「UserRoomDatabase」を返すgetDatabaseメソッドです。
fun getDatabase(context: Context): UserRoomDatabase {
return INSTANCE ?: synchronized(this) {
...
}
}
「エルビス演算子(?:)」は、左側の値がnullの場合、右側の値を返します。
例えば、「x = a ?: b」の場合、xはaがnullだとb、aがnull以外だとaになります。
このコードの場合は、INSTANCEがnullのとき、synchronized(this){}が実行されます。
つまり、データベースが存在するときは、存在するデータベースを返し、存在しない場合は、データベースを作成します。
データベースを作成するとき、複数のスレッドが同時にインスタンスを要求した場合、データベースが1つ以上存在する可能性があります。
これを防ぐのが、「synchronized(this){}」です。
synchronizedでコードをラップすることで、ブロック内には一度に1つの実行スレッドしか入れないようにすることができます。
return INSTANCE ?: synchronized(this) {
//ここには一度に1つのスレッドしかアクセスできない
}
これにより、複数のスレッドが同時にデータベースのインスタンスを要求しても、アクセスできるスレッドは1つのみなので、データベースが初期化されるのは1度だけにすることができます。
Room.databaseBuilder
「databaseBuilder(Context context, Class<T> class, String name)」を使ってデータベースを取得します。
val instance = Room.databaseBuilder(
context.applicationContext,
UserRoomDatabase::class.java,
"user_database"
)
.fallbackToDestructiveMigration()
.build()
- context: データベースのコンテキスト。通常はアプリケーションのコンテキスト
- class:@Databaseアノテーションがついた、RoomDatabaseを継承したクラス
- name:データベースファイルの名前
データベースのスキーマを変更してバージョンアップする場合、どのように移行するかを提供する必要があります。その移行戦略が「.fallbackToDestructiveMigration()」です。
「.fallbackToDestructiveMigration()」は、移行するときに古いデータベースを破棄して再構築します。
最後に「.build()」を呼び出します。
まとめ
データベースのライブラリのRoomについてまとめました。
- Roomを使うためには4つのことを行う。
- エンティティは、テーブルの列を定義する
- DAOは、データベースにアクセスするクエリを書く。
- データベースは、エンティティとDAOを定義する。
Roomはエンティティ、DAO、データベースで作成しますが、実際にRoomを使うにはViewModelを使うことが多いです。
Android開発のためのオススメ書籍
設計についてのオススメの書籍
参考サイト
Roomを使用してローカルデータベースにデータを保存する Androidデベロッパー