知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day19【Create a Room database編】
イントロダクション
ほとんどのアプリには、ユーザがアプリを閉じた後でも保持する必要のあるデータがあります。たとえば、アプリには、プレイリスト、ゲームアイテムの在庫、経費と収入の記録、星座のカタログ、または刑事的な睡眠データが保持される場合があります。通常、データベースを使用して永続データを保存します。
Room
は、Android Jetpack の一部であるデータベースライブラリです。Room
は、データベースのセットアップと構成の雑用を多く処理し、アプリが通常の関数呼び出しを使用してデータベースと対話できるようにします。内部的には、Room
は SQLite データベースの上にある抽象化レイヤーです。Room
の用語、および複雑なクエリのクエリ構文は、SQLite モデルに従います。
以下の画像は、Room
データベースがこのコースで推奨される全体的なアーキテクチャにどのように適合するかを示しています。
学ぶこと
Room
データベースを作成して操作し、データを永続化する方法- データベース内のテーブルを定義するデータクラスを作成する方法
- データアクセスオブジェクト(DAO)を使用して Kotlin 関数を SQL クエリにマップする方法
- データベースが機能しているかどうかをテストする方法
すること
- 夜間の睡眠データ用のインターフェースを備えた
Room
データベースを作成する - 提供されているテストを使用してデータベースをテストする
アプリ概要
このコードラボでは、睡眠の質を追跡するアプリのデータベース部分を構築します。アプリはデータベースを使用して、睡眠データを経時的に保存します。
次の図に示すように、アプリにはフラグメントで表される2つの画面があります。
左側に表示されている最初の画面には、追跡を開始および停止するためのボタンがあります。画面には、全てのユーザの睡眠データが表示されます。Clear ボタンは、アプリがユーザーのために収集した全てのデータを完全に削除します。
左側に表示されている2番目の画面は、睡眠の質の評価を選択するためのものです。アプリでは、評価は数値で表されます。開発の目的で、アプリは顔のアイコンとそれに相当する数値の両方を表示します。
ユーザーのフローは次の通りです:
- ユーザーがアプリを開くと、睡眠追跡画面が表示されます。
- ユーザーが Start ボタンをタップします。開始時刻を記録して表示します。Start ボタンが無効になり、Stop ボタンが有効になります。
- ユーザーが Stop ボタンをタップします。これにより、終了時刻が記録され、睡眠品質画面が開きます。
- ユーザーが睡眠品質アイコンを選択します。画面が閉じ、追跡画面に睡眠終了時間と睡眠の質が表示されます。Stop ボタンが無効になり、Start ボタンが有効になります。アプリは別の夜の準備ができています。
- データベースにデータがある場合は常に、Clear ボタンが有効になります。ユーザが Clear ボタンをタップすると、全てのデータが消去されます。"Are you sure?" というメッセージは表示されません。
このアプリは、完全なアーキテクチャのコンテキストで以下に示すように、簡略化されたアーキテクチャを使用します。アプリは次のコンポーネントのみを使用します。
- UI controller
- ViewModel と LiveData
- Room database
スターターアプリをダウンロードして検査する
スターターアプリをダウンロードして実行します
TrackMySleepQuality アプリを Github からダウンロードします。
アプリをビルドして実行します。アプリは
SleepTrackerFragment
fragment の UI を表示しますが、データは表示しません。ボタンはタップに反応しません。
スターターアプリを検査します
Tip: スターターアプリに精通していると、問題が発生した場合に、問題を特定して修正するのが簡単になります。
Gradle ファイルを見てください:
The project Gradle file プロジェクトレベルの
build.gradle
ファイルで、ライブラリのバージョンを指定する変数に注目してください。スターターアプリで使用されるバージョンは一緒にうまく機能し、このアプリでうまく機能します。このコードラボを終了するまでに、AndroidStudio から一部のバージョンの更新を求めるメッセージが表示される場合があります。アプリにあるバージョンを更新するか、そのままにするかはあなた次第です。奇妙なコンパイルエラーが発生した場合は、最終的な solution app がしようするライブラリバージョンの組み合わせを使用して見てください。The module Gradle file
Room
を含む全ての Android Jetpack ライブラリに提供されている依存関係と、コルーチンの依存関係に注意してください。packages と UI を見てください。アプリには機能によって構成されています。パッケージには、この一連のコードラボ全体でコードを追加するプレースホルダーファイルが含まれています。
Room
データベースに関連する全てのコード用のdatabase
packagesleepquality
およびsleeptracker
パッケージには、各画面の fragment、view、view model および view model factory が含まれています。睡眠品質データの表示に役立つ関数が含まれている
Util.kt
ファイルを見てください。一部のコードは、あとで作成する view model を参照しているため、コメントアウトされています。androidTest フォルダー(
SleepDatabaseTest.kt
) を見てください。このテストを使用して、データベースが意図した通りに機能することを確認します。
SleepNight Entity を作成します
Android では、データはデータクラスで表され、関数呼び出しを使用してデータにアクセスして変更します。ただし、データベースの世界では、entities と queries が必要です。
- Entity は、データベースに格納するオブジェクトまたは概念、およびそのプロパティを表します。Entity クラスはテーブルを定義し、そのクラスの各インスタンスはテーブルの行を表します。各プロパティは列を定義します。アプリでは、Entity は睡眠の夜に関する情報を保持します。
- クエリは、データベーステーブルまたはテーブルの組み合わせからのデータまたは情報の要求、またはデータに対してアクションを実行するための要求です。一般的なクエリは、Entity の取得、挿入、および更新です。例えば、開始時刻で並べ替えて、記録されている全ての睡眠の夜を照会できます。
Room
は、Kotlin データクラスから SQLite テーブルに格納できる Entity に、そして関数宣言から SQLクエリにいたるまで、全ての大変の作業を行います。
各 Entity を注釈付きデータクラスとして定義し、相互作用を注釈付きインターフェイスであるデータアクセスオブジェクト(DAO)として定義する必要があります。Room
は、これらの注釈付きクラスを使用して、データベースにテーブルを作成し、データベースに作用するクエリを作成します。
SleepNight Entity を作成します
このタスクでは、1日の睡眠を 注釈付きデータクラスとして定義します。
一晩の睡眠のために、開始時間、終了時間、そして品質評価を記録する必要があります。
また、夜を一意に識別するための ID が必要です。
database
パッケージで、SleepNight.kt
ファイルを見つけて開きます。ID、開始時間、終了時間、および数値の睡眠品質評価のパラメータを使用して、
SleepNight
データクラスを作成します。sleepQuality
を初期化する必要があるため、-1
に設定して、品質データが収集されていないことを示します。- また、終了時刻を初期化する必要があります。開始時刻に設定すると、終了時刻がまだ記録されていないことを通知します。
data class SleepNight( var nightId: Long = 0L, val startTimeMilli: Long = System.currentTimeMillis(), var endTimeMilli: Long = startTimeMilli, var sleepQuality: Int = -1 )
- クラスの宣言の前に、データクラスに
@Entity
アノテーションを付けます。テーブルにdaily_sleep_quality_table
という名前を付けます。tableName
の引数はオプションですが、推奨されます。ドキュメントで他の引数を調べることができます。
プロンプトが表示されたら、androidx
ライブラリから Entity
と他の全ての注釈をインポートします。
@Entity(tableName = "daily_sleep_quality_table") data class SleepNight(...)
nightId
を主キーとして識別するには、nightId
プロパティに@PrimaryKey
アノテーションを付けます。Room
が各 Entity の ID を生成するように、パラメータにautoGenerate
をtrue
に設定します。これにより、各夜の ID が一意であることが保証されます。
@PrimaryKey(autoGenerate = true) var nightId: Long = 0L,...
- 残りのプロパティに
@ColumnInfo
アノテーションを付けます。以下に示すように、パラメータを使用してプロパティ名をカスタマイズします。
import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "daily_sleep_quality_table") data class SleepNight( @PrimaryKey(autoGenerate = true) var nightId: Long = 0L, @ColumnInfo(name = "start_time_milli") val startTimeMilli: Long = System.currentTimeMillis(), @ColumnInfo(name = "end_time_milli") var endTimeMilli: Long = startTimeMilli, @ColumnInfo(name = "quality_rating") var sleepQuality: Int = -1 )
- コードをビルドして実行し、エラーがないことを確認します。
DAO を作成する
このタスクでは、data access object(DAO)を定義します。Android では、DAO はデータベースを挿入、削除、および更新するための便利なメソッドを提供します。
Room
データベースを使用する場合は、コードで Kotlin 関数を定義して呼び出すことにより、database に query を実行します。これらの Kotlin 関数は SQL クエリにマップされます。annotations を使用して DAO でこれらのマッピングを定義すると、Room
が必要なコードを作成します。
DAO は、database にアクセスするためのカスタムインターフェースを定義するものと考えてください。
一般的な database 操作の場合、Room
ライブラリは、@Insert
、@Delete
、@Update
などの便利なアノテーションを提供します。それ以外の場合は、@Query
アノテーションがあります。SQLite でサポートされている任意のクエリを記述できます。
追加のボーナスとして、Android Studio でクエリを作成すると、コンパイラは SQL クエリ構文エラーをチェックします。
sleep-tracker データベースの場合、次のことができる必要があります。
- Insert new nights を挿入します
- Update 既存の夜の終了時間と品質を更新する
- Get キーに基づいて特定の夜を取得する
- Get all nights
- Get the most recent night
- Delete データベース内の全てのエントリを削除する
SleepDatabaseDAO を作成する
database
package で、SleepDatabaseDao.kt
を開きます。interface
SleepDatabaseDao
に@Dao
アノテーションが付けられていることに注意してください。全ての DAO には、@Dao
キーワードのアノテーションをつける必要があります。
@Dao interface SleepDatabaseDao{}
- インターフェース内に
@Insert
アノテーションを追加します。@Insert
の下に、Entity
クラスSleepNight
のインスタンスを引数として取るinsert()
関数を追加します。
Room
は、SleepNight
をデータベースに挿入するために必要な全てのコードを生成します。Kotlin コードから insert()
を呼び出すと、Room
は SQLクエリを実行して、Entity をデータベースに挿入します。(Note: 関数は任意に呼び出すことができます。)
@Insert fun insert(night: SleepNight)
- 1つの
SleepNight
のupdate()
関数を使用して@Update
アノテーションを追加します。更新される Entity は、渡されたものと同じキーを持つ Entity です。Entity は他のプロパティの一部または全てを更新できます。
@Update fun update(night: SleepNight)
残りの機能には便利なアノテーションがないため、@Query
アノテーションを使用して SQLite クエリを提供する必要があります。
Long
キー引数をとり、nullable のSleepNight
を返すget()
関数を@Query
アノテーションと定義します。パラメータがない場合はエラーが表示されます。
@Query fun get(key: Long): SleepNight?
query は、annotation への文字列パラメータとして提供されます。
@Query
にパラメータを追加します。SQLite クエリである文字列にします。daily_sleep_quality_table
から全ての列を選択しますWHERE
thenaightId
がkey
引数と一致する
:key
に注目してください。クエリでコロン表記を使用して、関数の引数を参照します。
("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
clear
関数と SQLite クエリを使用して別の@Query
を追加し、daily_sleep_quality_table
から全てをDELETE
します。このクエリはテーブル自体を削除しません。
@Delete
アノテーションは1つのアイテムを削除し、@Delete
を使用して、削除する夜のリストを指定できます。欠点は、テーブルの内容をフェッチまたは知っている必要があることです。@Delete
アノテーションは特定のエントリを削除するのに最適ですが、テーブルから全てのエントリをクリアするには効率的ではありません。
@Query("DELETE FROM daily_sleep_quality_table") fun clear()
getTonight()
関数を使用して@Query
を追加します。getTonight()
によって返されるSleepNight
を nullable にして、テーブルが空の場合に関数が処理できるようにします。(テーブルは最初とデータがクリアされた後は空です。)
データベースから "tonight" を取得するには、nightId
で降順に並べ替えられた結果のリストの最初の要素を返す SQLite クエリを記述します。LIMIT 1
を使用して、1つの要素のみを返します。
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1") fun getTonight(): SleepNight?
getAllNights()
関数を使用して@Query
を追加します。SQLite クエリが
daily_sleep_quality_table
から全ての列を降順で返すようにします。getAllNights()
にSleepNight
Entity のリストをLiveData
として返すようにします。Room
は、このLiveData
を更新し続けます。つまり、データを明示的に取得する必要があるのは一回だけです。androidx.lifecycle.LiveData
からLiveData
をインポートする必要がある場合があります。
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC") fun getAllNights(): LiveData<List<SleepNight>>
- 目に見える変化は見られませんが、アプリを実行してエラーがないことを確認してください。
Room データベースを作成してテストする
このタスクでは、前のタスクで作成した Entity
と DAO を使用する Room
データベースを作成します。
@Database
アノテーションが付けられた抽象データベースホルダークラスを作成する必要があります。このクラスには、データベースが存在しない場合にデータベースのインスタンス を作成するか、既存のデータベースへの参照を返すメソッドが1つあります。
Room
データベースの取得は少し複雑なので、コードを開始する前の一般的なプロセスは次の通りです:
RoomDatabase
を拡張するpublic abstract
クラスを作成します。このクラスは、データベースホルダーとして機能します。Room
が実装を作成するため、クラスは抽象的です。- クラスに
@Database
アノテーションを付けます。引数で、database の Entity を宣言し、version number を設定します。 companion
オブジェクト内で、SleepDatabaseDao
を返す abstract method または property を定義します。Room
は body を生成します。- アプリ全体で必要な
Room
データベースのインスタンスは1つだけなので、RoomDatabase
をシングルトンにします。 - database が存在しない場合にのみ、
Room
の database builder を使用してデータベースを作成します。それ以外の場合は、既存のデータベースを返します。
Tip:
コードはどの Room
データベースでもほぼ同じであるため、このコードをテンプレートとして使用できます。
Database を作成する
database
package で、SleepDatabase.kt
を開きます。- ファイルに、
RoomDatabase
を拡張するSleepDatabase
という抽象クラスを作成します。
クラスに @Database
アノテーションをつける。
@Database() abstract class SleepDatabase : RoomDatabase() {}
Entity と version prameter が欠落しているとエラーが表示されます。
Room
がデータベースを構築できるように、@Database
アノテーションにはいくつかの引数が必要です。Entity
のリストを持つ唯一のアイテムとしてSleepNight
を提供します。version
を1
に設定します。schema を変更するたびに、version number を増やす必要があります。- Schema の version history のバックアップを保持しないように、
exportSchema
をfalse
に設定します。
entities = [SleepNight::class], version = 1, exportSchema = false
- データベースは DAO について知る必要があります。クラス内で、
SleepDatabaseDao
を返す抽象値を宣言します。複数の DAO を持つことができます。
abstract val sleepDatabaseDao: SleepDatabaseDao
- その下に、
companion
オブジェクトを定義します。companion オブジェクトを使用すると、クライアントはクラスをインスタンス化せずにデータベースを作成または取得するためのメソッドにアクセスできます。このクラスの唯一の目的はデータベースを提供することであるため、データベースをインスタンス化する理由はありません。
companion object {}
companion
オブジェクト内で、データベースのprivate
nullable 変数INSTANCE
を宣言し、それをnull
で初期化します。INSTANCE
変数は、データベースが作成されると、データベースへの参照を保持します。これにより、コストのかかるデータベースへの接続を繰り返し開くことを回避できます。
INSTANCE
に @Volatile
アノテーションを付けます。揮発性変数の値がキャッシュされることはなく、全ての書き込みと読み取りはメインメモリとの間で行われます。これにより、INSTANCE
の値が常に最新であり、全ての実行スレッドで同じであることを確認できます。つまり、1つのスレッドによって INSTANCE
に加えられた変更は、他の全てのスレッドにすぐに表示され、例えば、2つのスレッドがそれぞれキャッシュ内の同じ Entity を更新して問題が発生するという状況は発生しません。
@Volatile private var INSTANCE: SleepDatabase? = null
INSTANCE
の下で、companion
オブジェクト内に、データベースビルダーが必要とするContext
パラメーターを使用してgetInstance()
メソッドを定義します。SleepDatabase
を返します。getInstance()
がまだ何も返さないため、エラーが表示されます。
fun getInstance(context: Context): SleepDatabase {}
getInstance()
内に、synchronized{}
ブロックを追加します。これを渡して、context にアクセスできるようにします。
複数のスレッドが同時にデータべースインスタンスを要求する可能性があり、その結果、データベースが1つではなく2つになります。このサンプルアプリではこの問題が発生する可能性は低いですが、より複雑なアプリでは発生する可能性があります。データベースを同期するためにコードをラップするということは、一度に1つの実行スレッドのみがこのコードのブロックに入ることができることを意味します。これにより、データベースが一回だけ初期化されるようになります。
synchronized(this) {}
- 同期されたブロック内で、
INSTANCE
の現在の値をローカル変数インスタンスにコピーします。これは、ローカル変数でのみ使用できる smart cast を利用するためです。
var instance = INSTANCE
synchronized
ブロック内で、synchronized
ブロックの最後にreturn instance
します。
return instance
if (instance == null) {}
instance
がnull
の場合は、データベースビルダーを使用してデータベースを使用してデータベースを取得します。if
ステートメントの body で、Room.databaseBuilder
を呼び出し、渡したコンテキスト、データベースクラス、およびデータベースの名前sleep_history_database
を指定します。
instance = Room.databaseBuilder( context.applicationContext, SleepDatabase::class.java, "sleep_history_database")
- 必要な migration strategy をビルダーに追加します。
.fallbackToDestructiveMigration()
を使用します。
通常、スキーマが変更された場合の migration strategy を移行オブジェクトに提供する必要があります。移行オブジェクトは、古いスキーマの全ての行を取得し、それらを新しいスキーマの行に変換して、データが失われないようにする方法を定義するオブジェクトです。Migration はこのコードラボの範囲を超えています。簡単な解決策は、データベースを破棄して再構築することです。これは、データが失われることを意味しています。
.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
データベースを操作するための全てのビルディングブロックができました。このコードはコンパイルされて実行されますが、実際に機能するかどうかを判断する方法はありません。したがってこれはいくつかの基本的なテストを追加する良い機会です。
SleepDatabase をテストする
このステップでは、提供されたテストを実行して、データベースが機能することを確認します。これにより、データベースを構築する前にデータベースが確実に機能するようになります。提供されているテストは基本的なものです。本番アプリの場合、全ての DAO で全ての機能とクエリを実行します。
スターターアプリには androidTest フォルダーが含まれています。この androidTest フォルダーには、Android インスチルメンテーションを含む単体テストが含まれています。これは、テストには Android フレームワークが必要であるため、物理デバイスまたは仮想デバイスでテストを実行する必要があることを示しています。もちろん、Android フレームワークを含まない純粋な単体テストを作成して実行することもできます。
Android Studio の androidTest フォルダーで、SleepDatabaseTest ファイルを開きます。
コードのコメントを解除します。
これは、再利用できる別のコードであるため、テストコードの簡単な説明です。
SleepDatabaseTest
はテストクラスです。@RunWith
アノテーションは、テストをセットアップして実行するプログラムであるテストランナーを識別します。- セットアップ中に、
@Before
アノテーションが付けられた関数が実行され、SleepDatabaseDao
を使用してメモリ内のSleepDatabase
が作成されます。"In-memory" とは、このデータベースがファイルシステムに保存されず、テストの実行後に削除されることを意味します。 - また、メインメモリデータベースを構築する場合、コードは別のテスト固有のメソッド
allowMainThredQueries
を呼び出します。デフォルトでは、メインスレッドでクエリを実行しようとするとエラーが発生します。このメソッドを使用すると、メインスレッドでテストを実行できます。これは、テスト中にのみ実行する必要があります。 @Test
アノテーションが付けられたテストメソッドで、SleepNight
を作成、挿入、取得し、それらが同じであることを表明します。何か問題が発生した場合は、例外をスローします。実際のテストでは、複数の@Test
メソッドがあります。テストが完了すると、
@After
アノテーションが付けられた関数が実行され、データベースが閉じられます。テストファイルを右クリックして、Run 'SleepDatabaseTest' を選択します。
- テストの実行後、
SleepDatabaseTest
ペインで、全てのテストをパスしたことを確認します。
全てのテストに合格したため、次のことが分かります。
- database は正しく作成されます
SleepNight
をデータベースに挿入できますSleepNight
を取得することができますSleepNight
の品質には正しい値があります
まとめ
- テーブルを
@Entity
アノテーションが付けられたデータクラスとして定義します。@ColumnInfo
アノテーションが付けられたプロパティをテーブルの列として定義します。 @Dao
アノテーションが付けられたインターフェースとしてデータアクセスオブジェクト(DAO)を定義します。DAO は、Kotlin 関数をデータベースクエリにマップします。- アノテーションを使用して、
@Insert
、@Delete
および@Update
関数を定義します。 - 他のクエリのパラメータとして、SQLite クエリ文字列と共に
@Query
アノテーションを使用します。 - データベースを返す
getInstance()
関数を持つ抽象クラスを返します。 - instrumented 化されたテストを使用して、データベースと DAO が期待通りに機能していることをテストします。提供されているテストテンプレートとして使用できます。