Android Kotlin基礎講座 06.1: Roomデータベースを作成する
目次
タスク:Roomデータベースを作成・テストする
このタスクでは、前のタスクで作成した実体とDAOを使用するRoomデータベースを作成します。
@Databaseでアノテーションされた抽象データベースホルダークラスを作成する必要があります。このクラスはデータベースが存在しない場合にデータベースのインスタンスを作成するか、存在するデータベースの参照を返すメソッドを持っています。
Roomデータベースを取得するのは少し複雑ですので、コードを書き始める前に、一般的なプロセスを紹介します。
- RoomDatabaseを継承するpublicな抽象クラスを作成します。このクラスはデータベースホルダーとして機能します。Roomが実装を作成するため、このクラスは抽象です。
- このクラスを@Databaseでアノテーションします。引数でデータベース用の実体を宣言し、バージョン番号を設定します。
- companionオブジェクト内で、SleepDatabaseDaoを返す抽象メソッド、またはプロパティを定義します。Roomが中身を生成してくれます。
- アプリ全体で必要なRoomデータベースのインスタンスは一つだけです。ですので、RoomDatabaseはシングルトンにします。
- データベースが存在していない場合のみ、データベースを作成するためにRoomのデータベースビルダーを使用します。そうでない場合は存在するデータベースを返します。
Tip: 他のRoomデータベースでもほとんどコードは同じになるので、このコードはテンプレートとして使用することができます。
ステップ1:データベースを作成する
- databaseパッケージのSleepDatabase.ktを開いてください。
- ファイル内で、SleepDatabaseという抽象クラスを作成し、RoomDatabaseを継承させてください。
@Databaseでクラスをアノテーションしてください。
@Database()
abstract class SleepDatabase : RoomDatabase() {}
- 実体とバージョンを指定するパラメータがないことに対するエラーが表示されます。@DatabaseアノテーションはRoomがデータベースを作成するためにいくつかの引数を要求します。
- 実体のリストとしてSleepNightを渡してください。
- バージョンは1に設定してください。スキーマを変更する際にはバージョン番号を上げる必要があります。
- exportSchemaはfalseに設定してください。これでスキーマバージョンヒストリーのバックアップが保存されないようになります。
entities = [SleepNight::class], version = 1, exportSchema = false
- データベースはさらにDAOについて知る必要があります。クラスの内側でSleepDatabaseDaoを返す抽象値を宣言してください。複数のDAOをもつこともできます。
abstract val sleepDatabaseDao: SleepDatabaseDao
- その下にcompanionオブジェクトを定義してください。companionオブジェクトはクラスのインスタンス化無しで、データベースを作成または取得するためのメソッドへのクライアントからのアクセスを可能にします。このクラスの唯一の目的はデータベースを提供することだけなので、これをインスタンス化する理由はないのです。
companion object {}
- companionオブジェクトの中で、プライベートでnull許容のデータベース用のINSTANCE変数を宣言し、nullで初期化してください。データベースが作成された時点でINSTANCE変数にはデータベースの参照を持たせます。これにより、繰り返し負荷のかかるデータベースへの接続を開く必要がなくなります。
INSTANCE変数を@Volatileでアノテーションしてください。volatile変数の値は決してキャッシュされず、全ての読み書きはメインメモリで行われます。これによりINSTANCEの値が常に最新かつ、全ての実行中のスレッドで同じことが保障されます。これはあるスレッドによってなされたINSTANCE変数への変更は他の全てのスレッドでも即座に反映され、よく問題が起こると言われる、二つ以上のスレッドがそれぞれキャッシュ内の同じ実体を更新してしまうといった状況に陥ることがなくなります。
@Volatile
private var INSTANCE: SleepDatabase? = null
- INSTANCEの下にgetInstance()メソッドをデータベースビルダーが必要とするContextパラメーターと共に定義してください。SleepDatabase型を返すようにします。getInstance()が何も返さないのでエラーが表示されます。
fun getInstance(context: Context): SleepDatabase {}
- getInstance()内に、synchronized{}ブロックを追加してください。thisを渡して、contextにアクセスできるようにしてください。
複数のスレッドがデータベースインスタンスに同時に接続する可能性があり、その結果データベースが一つではなく二つになってしまいます。この問題はこのサンプルアプリでは起こることは考えにくいですが、より複雑なアプリでは起こり得ます。synchronizedでデータベースを取得するためのコードをラッピングすることで実行中の一つのスレッドのみ、このコードのブロックに突入することができ、データベースが一度しか初期化されないことが保障されます。
synchronized(this) {}
- synchronizedブロック内で、INSTANCEの現在の値をローカル変数instanceにコピーしてください。これはスマートキャストの利点を利用するためで、それはローカル変数にのみ使えるためです。
var instance = INSTANCE
- synchronizedブロック内の最後で、instanceを返します。ここでは型ミスマッチエラーは無視してください。
return instance
- return文の上に、instanceがnullかをチェックするif文を追加してください。nullというのはデータベースがまだ存在していないことを意味します。
if (instance == null) {}
- もしinstanceがnullの場合、データベースを取得するためにデータベースビルダーを使用します。if文の中で、Room.databaseBuilderを呼び出し、先ほど渡したcontext、データベースクラス、データベースの名前であるsleep_history_databaseを渡してください。エラーを消すためには次の手順で移行戦略とbuild()を追加する必要があります。
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database")
- ビルダーに要求されている移行戦略を追加します。.fallbackToDestructiveMigration()を使います。
普通はスキーマが変わったとき用に移行オブジェクトを移行戦略と共に提供する必要があります。移行オブジェクトとはデータが失われずに、以前のスキーマの全ての行を新しいスキーマの行に変換する方法を定義するオブジェクトです。
移行はこの記事の範囲を超えています。簡単な解決方法はデータベースを破棄、リビルドすることですが、これはデータが失われることを意味します。
.fallbackToDestructiveMigration()
- 最後に.build()を呼び出してください。
.build()
- 最後のステップとしてif文の中でINSTANCE = instanceを代入してください。
INSTANCE = instance
- 最終的なコードは以下のようになります。
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
abstract val sleepDatabaseDao: SleepDatabaseDao
companion object {
@Volatile
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
- ビルドしてアプリを起動してください。
これでRoomデータベースを機能させるためのビルディングブロックが全て揃いました。このコードはコンパイルし、動作しますが、実際に動作しているのか確かめる術がありません。ですので、基本的なテストを追加していきましょう。
ステップ2:SleepDatabaseをテストする
このステップではデータベースが機能しているのかを確かめるためのテストを行っていきます。これはデータベースを構築する前にデータベースが機能しているかを確認するのに役立ちます。提供されているテストは基本的なものです。製品としてのアプリのためには、DAO内の全ての関数のクエリを試すことになります。
スターターアプリにはandroidTestフォルダーが含まれています。このandroidTestフォルダーにはAndroidインストルメンテーションに関係するユニットテストが含まれており、これはAndroidフレームワークを必要とするため、実機かバーチャルデバイス上でテストを実施する必要があります。もちろん、Androidフレームワークに関わらない純粋なユニットテストを作成、実施することもできます。
- androidTestフォルダーのSleepDatabaseTestファイルを開いてください。
- コード内のコメントアウトを解除してください。全てのコメントアウトされたコードを選択した状態でControl + /でもコメントアウトを解除できます。
- ファイルを見てください。
以下はテストコードの概要です。
- SleepDatabaseTestはテスト用クラスです。
- @Runwithアノテーションはテストをセットアップし実行するテストランナーを識別します。
- セットアップの間、@Beforeでアノテーションされた関数が実行され、インメモリのSleepDatabaseとSleepDatabaseDaoが作成されます。”インメモリ”はこのデータベースがファイルシステムに保存されず、テスト実行後に削除されるということを意味します。
- またインメモリデータベースの作成中に、コードは別のテスト用のメソッドであるallowMainThreadQueriesを呼び出します。デフォルトではメインスレッドでクエリを実行しようとした場合、エラーが発生します。このメソッドはメインスレッド上でテストを実行できるようにしてくれます。これはテスト中のみ行うべきものです。
- @Testでアノテーションされたテストメソッド内では、SleepNightを作成、挿入、取得し、それらが同じであることを確かめます。もし何かがうまくいかない場合は、例外を投げます。実際のテストでは複数の@Testメソッドを持つことになるでしょう。
- テストが完了したら、@Afterでアノテーションされた関数がデータベースを閉じるために実行されます。
- プロジェクトパネル内でテストファイルを右クリックし、Run ‘SeepDatabaseTest’を選択してください。
- テスト終了後、SleepDatabaseTestパネル内で、全てのテストが完了したことを確認してください。
全てのテストが完了したので、以下のことを知ることができました。
- データベースが正確に作成されている。
- SleepNightをデータベースに挿入することができる。
- SleepNightを取得できる。
- SleepNightが睡眠の質に対する正しい値を持っている。
完成済みプロジェクト
お疲れさまでした。完成済みプロジェクトは以下からダウンロードできます。