新型コロナの影響で自宅待機になってしまい、その間勉強するものとしてSunflower
リポジトリを
勧めてもらいました。
JetPackのライブラリのうち、今回はRoom
編です
尚、引用しているソースは明記しているところ以外は、基本的には全てSunflower
のリポジトリのものです。
環境
- 確認時は
Android Studio
のバージョンは3.6.2
を使用しました -
JetPack
はAndroidX
ライブラリを利用するのでCompile SDK
を28
以上にする必要があります
そもそもRoom
ってなに?
公式の説明
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、
データベースへのより安定したアクセスを可能にし、
SQLite を最大限に活用できるようにします。
です!
Room
を使う理由
- アプリ内ではオブジェクトを利用して、
SQLite
のデータベースにアクセスできる。 - コンパイル時にチェックができる
(Sunflower
のリポジトリのトップページのRoom
の説明より)
利用する際に必要なこと
必要なことは下記の4点です
- 依存関係の記載
- データベース作成
- エンティティ作成
- DAO作成
依存関係の記載
build.gradle
に記載する内容
build.gradle
には下記の依存関係の記載を行います。
(公式ドキュメントより)
dependencies {
def room_version = "2.2.3"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
- 注意点
-
room-compiler
の部分については、Kotlin
で使用する場合はannotationProcessor
ではなく、kapt
にします
-
- オプション設定
Kotlin
RxJava
Guava
-
Test helpers
- これらはオプションなので、利用する場合は記載します。
Sunflower
リポジトリの場合
Sunflower
リポジトリの場合はどのようになっているかを見てみましょう
buildscript {
// Define versions in a single place
ext {
:
// App dependencies
:
roomVersion = '2.1.0'
:
}
:
}
dependencies {
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
:
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
:
}
Sunflower
リポジトリの場合は、Kotlin
のオプションのみ利用しているようです。
データベースのクラス作成
/**
* The Room database for this app
*/
@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun gardenPlantingDao(): GardenPlantingDao
abstract fun plantDao(): PlantDao
データベースの設定を行うクラスをみてみましょう。
データベース利用の際の設定的なものは、ここの最初の部分に書かれています。
-
クラスの宣言に
@Database
アノテーションを記載します(必須)- データベースに関連付けられているエンティティのリストをアノテーション内に含む必要があり、
entities
にそれを記載します。(必須)- ここでは、
GardenPlanting
クラスとPlant
クラスが記載されています。
- ここでは、
-
version
はデータベースのバージョンを定義できます。マイグレーション処理を書く際はこちらのバージョンを参照できます(任意) -
exportSheme
はfalse
に設定しています。(任意)- こちらは
default
でtrue
に設定されます。 -
true
に設定された場合はroom.schemaLocation
がbuild.gradle
に定義されている場合は、データベースのスキーマ情報をJSONファイルにエクスポートします。それを利用するとマイグレーションのテストが実施できます。 - 詳しくはこちら
- こちらは
- データベースに関連付けられているエンティティのリストをアノテーション内に含む必要があり、
-
@TypeConverter
アノテーションはentity
に保存できない型がある場合(例えば、GardenPlanting
クラスのplantDate
はCalender型)
)、変換処理を実装したクラスをつくり、そのクラス名を記載します。(任意)- ここでは、
Converter
クラスにCalender
↔︎long
の相互変換処理が記載されており、そのConverter
クラスを記載しています。
- ここでは、
-
RoomDatabase
を拡張する抽象クラスとしてクラスを宣言します(必須)-
abstract class AppDatabase : RoomDatabase()
として宣言しています。
-
-
引数が 0 で、
@Dao
アノテーション付きのクラスを返す抽象メソッドをクラス内に記載します(必須)-
gardenPlantingDao()
メソッドとplantDao()
メソッドを記載してあります。
-
companion object {
// For Singleton instantiation
@Volatile private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
こちらは、シングルトンで利用するための内容が記載されています。
すでにインスタンスが作られている場合は、作られているものを使い、ない場合は作るという処理です
スレッドセーフにするためにsynchronized()
で排他制御を行っています。
// Create and pre-populate the database. See this article for more details:
// https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance(context).enqueue(request)
}
})
.build()
}
}
}
こちらでは、データベースを作成する処理です。
-
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME).build
でデータベースのインスタンスを作成します。 - その間に
addCallback()
メソッドが入っています。- こちらはデータベースが作成された時に実施されるコールバックメソッドです。
- 例えば、データベースの初期データなどを読み込む必要がある場合などで利用できます。
- ここでは
WorkManager
を利用し、バックグラウンド処理でjsonファイルから初期値の情報を読み込んで、データベースに保存しています。
エンティティの定義
それでは、Sunflower
リポジトリのGardenPlanting
クラスのエンティティ定義からみてみましょう
@Entity(
tableName = "garden_plantings",
foreignKeys = [
ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
],
indices = [Index("plant_id")]
)
-
@Entity
のアノテーションをつけてクラスを作成します。(必須)-
tableName
に指定した名称が、SQLite
データベース内のテーブル名になります。もし、なにも記載しなかった場合はクラス名がテーブル名になります。(任意) -
foreignKeys
は外部キー制約の定義を行います。ここでは、Plant
クラスのid
にある値のみ、plant_id
に設定可能となります。(任意) -
indices
で、このテーブルに設定するインデックスを指定します。この場合はplant_id
をインデックスに設定しています。(任意)
-
data class GardenPlanting(
@ColumnInfo(name = "plant_id") val plantId: String,
/**
* Indicates when the [Plant] was planted. Used for showing notification when it's time
* to harvest the plant.
*/
@ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),
/**
* Indicates when the [Plant] was last watered. Used for showing notification when it's
* time to water the plant.
*/
@ColumnInfo(name = "last_watering_date")
val lastWateringDate: Calendar = Calendar.getInstance()
) {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
var gardenPlantingId: Long = 0
}
-
@ColumnInfo
で各カラムの定義を行います。変数宣言などは通常の場合と同一です。(必須) -
@ColumnInfo
アノテーションの中のname
でカラム名を定義します。(任意)- テーブル名と同様に、指定しなかった場合は変数名がカラム名となります。
- ここでは
plantId
,plantDate
,lastWateringDate
の3つの引数を指定するコンストラクターを定義しています。
- プライマリーキーには
@PrimaryKey
のアノテーションをつけて変数を定義します。(任意)- カラム名
id
、変数名gardenPlantId
がプライマリーキーに指定されています。 - ここでは
autoGenerate = true
を指定し、自動的に割り当てるようになっています。
- カラム名
DAO
の定義
次にGardenPlantingDao
をみてみましょう。
DAO
とはData Access Object
の略でデータベースの操作のインターフェースを提供するオブジェクトです。
使う側はデータベースの詳細を知ることなくデータの保存や取得が利用可能です。
/**
* The Data Access Object for the [GardenPlanting] class.
*/
@Dao
interface GardenPlantingDao {
-
@Dao
アノテーションをつけたインターフェースを定義します。(必須)
@Query("SELECT * FROM garden_plantings")
fun getGardenPlantings(): LiveData<List<GardenPlanting>>
@Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
fun isPlanted(plantId: String): LiveData<Boolean>
/**
* This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle
* the object mapping.
*/
@Transaction
@Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
fun getPlantedGardens(): LiveData<List<PlantAndGardenPlantings>>
-
@Query
アノテーションは、データベースに対して読み書き処理を実行できます。- コンパイル時に検証されるので、クエリに問題があると、コンパイルエラーになります
- 戻り値についても検証を行い、誤りがある場合は警告またはエラーを表示します
- クエリにパラメータを渡す場合は、
:[メソッドの引数名]
としてクエリ内に記載します- ここでは2つめのクエリの中の
:plantId
がそのように使われています
- ここでは2つめのクエリの中の
-
@Transaction
アノテーションで記載したメソッドは、全て同一トランザクションで実行します - 各メソッドの戻り値に
LiveData
を指定することができます。LiveData
とRoom
の組み合わせで、例えば、データベースのテーブルが更新された際にView
の更新などができます。詳しくはLiveData編をご覧ください
@Insert
suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long
@Delete
suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
}
-
@Insert
アノテーションをつけたメソッドは、データベースに挿入する実装がRoom
によって作成されます。- 複数のパラメータを指定することができます。
- 全てのパラメータを同一のトランザクションで実行します
- 戻り値を指定すると、挿入されるアイテムの新しい
rowId
になるlong
の値を返すことができます。-
rowId
の詳細についてはこちら
-
-
@Delete
アノテーションをつけたメソッドは、こちらも、@Insert
と同様に、データベースないの指定したエンティティのセットを削除する処理を作成します。 -
ここでは使用されていませんが、
@Update
も同様です -
メソッド名の前に
suspend
がついているものは、コルーチン機能を使用した際に非同期かすることができ、メインスレッド上で実行される可能性がなくなります。(サブスレッドで実行して下さい、の意)
参考サイト
- Sunflowerリポジトリ
- ViewModel: 公式ドキュメント
- LiveData: 公式ドキュメント
- DataBinding: 公式ドキュメント
- Room: 公式ドキュメント
-
MySQL 外部キ−制約:
@IT
の記事 - SQLite インデックス: SQLiteのインデックスについて記載されているサイト
- SQLite rowId: SQLiteのrowIdについて
- Room Dao: Room Dao公式ドキュメント
- Room マイグレーション: Roomマイグレーション