Roomの三種の神器の作り方
Room利用の準備が整ったところで、前節で紹介したRoomに必要な3個の部品の作り方を紹介していきます。
Java版エンティティ
先述のように、エンティティはテーブル構造を表すJava/Kotlinクラスです。ここで、例えば、表1のようなカクテルデータを管理するcocktailmemosテーブルを想定すると、エンティティクラスのJavaコードは、リスト3のようになります。
カラム名 | データ型 | 内容 |
---|---|---|
id | 整数値 | 主キー |
name | 文字列 | カクテル名 |
note | 文字列 | メモ |
@Entity(tableName = "cocktailmemos") // (1) public class Cocktailmemo { // (2) @PrimaryKey // (3) public long id; // (4) @NonNull // (5) public String name; // (6) public String note; // (7) }
テーブル名がcocktailmemosですので、(2)のように、これを単数系のキャメル記法に変換したものがクラス名となります。ただし、(1)のように@Entityアノテーションを記述する必要があります。これがないとエンティティと認識されません。その際、tableNameプロパティを指定すると、指定されたテーブル名で作成されます。この指定がない場合は、クラス名がテーブル名になります。
このエンティティクラス内に、カラムに対応するpublicフィールドを定義します。その際、フィールドのデータ型とフィールド名が、そのままテーブルのカラムに適用されます。例えば、(4)はフィールド名がidでデータ型がlongなので、整数値型のカラムidとなります。(6)と(7)も同様に、文字列型のnameとnoteが生成され、結果的に表1のテーブルが作成されることになります。
さらに、主キーカラムに対しては、(3)のように@PrimaryKeyアノテーションを記述します。もし、オートインクリメントの主キーにしたい場合は、以下のようにします。
@PrimaryKey(autoGenerate = true)
また、NOT NULLカラムに対しては、(5)のように@NonNullアノテーションを記述します。デフォルト値を設定したい場合は、以下のようにします。
@ColumnInfo(defaultValue = "…")
この@ColumnInfoでは、さらに、以下のようにnameプロパティを利用することで、フィールド名とは別のカラム名を指定することもできます。
@ColumnInfo(name = "…")
Kotlin版エンティティ
このエンティティをKotlinで定義する場合は、リスト4のようなコードとなります。
@Entity(tableName = "cocktailmemos") // (1) class Cocktailmemo ( // (2) @PrimaryKey val id: Int, // (3) val name: String, // (4) val note: String? // (5) )
リスト4の(1)のように、クラスアノテーションとして@Entityを記述するところは、Javaコードと同じです。違うのは、フィールドとしてカラムを定義する代わりに、(2)のようにプライマリーコンストラクタの引数として、カラムに該当するプロパティを定義する点です。(3)が主キーidを定義しているコードであり、valの前にアノテーションを記述しています。また、Kotlinは変数宣言そのものにnull許容かどうかを定義できるので、(4)のように通常の引数宣言とすると、@NonNullアノテーションの代わりになります。逆に、(5)のように「?」を記述することで、NULL許容のカラムとなります。
Java版DAOインターフェース
エンティティの作成が終了したところで、三種の神器の2個目であるDAOの作成を行なっていきます。このDAOは、先述の通り、インターフェースとして定義します。例えば、前項で定義したcocktailmemosテーブルのデータ操作として、表2の処理を考えたとします。
操作内容 | メソッド名 | 戻り値の内容 |
---|---|---|
主キーでの検索 | findByPK | Cocktailmemoエンティティ |
登録 | insert | 登録された主キーを表すlong値 |
削除 | delete | 削除件数を表すint値 |
これらの操作内容を定義したDAOインターフェースは、Javaコードではリスト5のようになります。
@Dao // (1) public interface CocktailmemoDAO { // (2) @Query("SELECT * FROM cocktailmemos WHERE id = :id") // (3) public ListenableFuture<Cocktailmemo> findByPK(int id); // (4) @Insert // (5) public ListenableFuture<Long> insert(Cocktailmemo cocktailmemo); // (6) @Delete // (7) public ListenableFuture<Integer> delete(Cocktailmemo cocktailmemo); // (8) }
まず、インターフェース名は、リスト5の(2)のように、「エンティティクラス名+DAO」とするとわかりやすいです。そして、そのインターフェース宣言に対して、(1)の@Daoアノテーションを記述する必要があります。
このインターフェース内に、各データ処理をメソッドとして定義します。(4)のfindByPK()、(6)のinsert()、(8)のdelete()として定義されたメソッドは、まさに表2の通りです。ただし、そのメソッドに対して、そのメソッドにおいて実行するSQL文を(3)のように@Queryアノテーションの()内に記述します。その際、バインド変数を利用する場合は、メソッドの引数を定義し、その引数名に:(コロン)を接頭辞としたものをプレースホルダとします。例えば、(4)のfindByPK()にはid引数が定義されているので、これにコロンをつけた:idをプレースホルダとしています。そして、引数の値が、このプレースホルダに埋め込まれた(バインドされた)上でSQLが実行されます。
なお、この@Queryで指定するSQL文はコンパイル段階でチェックされ、構文エラーはコンパイルエラーの形で指摘してくれます。存在しないテーブルやカラムの指定も、同じくコンパイルエラーとなるので、便利です。
この@Queryアノテーションには、SELECT文だけでなく、INSERT/UPDATE/DELETEの各SQL文を記述することができますが、単純なINSERT/UPDATE/DELETEの場合は、(5)や(7)のように、専用のアノテーションが用意されていますので、これらを利用できます。ただし、その場合は、メソッドの引数は、(6)や(8)のようにエンティティオブジェクトとなります。
このようにしてDAOメソッドを定義する際、1点注意点があります。それは、戻り値はそのまま指定できないことです。Roomでは、これらのSQL処理を非同期で実行することとなっています。それに対応するために、SQL実行の結果を表す戻り値は、ListenableFutureオブジェクトでラップする必要が出てきます。(4)や(6)や(8)の戻り値が、表2の戻り値の記載通りではなく、全て、ListenableFuture<Cocktailmemo>のように、本来の戻り値をジェネリクスとして型指定している記述になっているのは、そのためです。リスト5には含まれていませんが、もし全件検索のように検索結果が複数行になる場合は、各要素をエンティティとするListオブジェクトとなり、戻り値の型はListenableFuture<List<Cocktailmemo>>のようになります。
なお、このListenableFutureは、Javaの非同期処理結果を格納するインターフェースであるFutureの子インターフェースであり、GoogleがリリースしているGuavaライブラリに含まれています。そのため、RoomのDAOをコーディングするためには、あらかじめこのGuavaライブラリの利用を設定しておく必要があります、リスト1の(4)はこのための記述であり、さらに、RoomとGuavaを連携させるために、同じくリスト1の(3)の設定が必要となります。
Kotlin版DAOインターフェース
さて、このDAOインターフェースをKotlinで定義すると、リスト6のようになります。
@Dao interface CocktailmemoDAO { @Query("SELECT * FROM cocktailmemos WHERE id = :id") suspend fun findByPK(id: Int): Cocktailmemo? @Insert suspend fun insert(cocktailmemo: Cocktailmemo): Long @Delete suspend fun delete(cocktailmemo: Cocktailmemo): Int }
アノテーションの記述方法はJavaと全く同じです。Javaコーディングと全く違うのは、非同期処理の扱いです。Kotlinには、Javaにはないコルーチンという仕組みがあります。これを利用しますので、各メソッドにはsuspendを記述します。このsuspendのおかげで、戻り値については、ListenableFutureオブジェクトでラップする必要はなくなります。
Java版Room Database
三種の神器の最後は、Room Databaseです。この部品をなぜRoom Databaseと呼ぶかは、ズバリ、RoomDatabaseクラスを継承した抽象クラスとして作成するからです。これは、Javaコードでは、リスト7のようになります。
@Database(entities = {Cocktailmemo.class}, version = 1) // (1) public abstract class AppDatabase extends RoomDatabase { // (2) private static AppDatabase _instance; public static AppDatabase getDatabase(Context context) { if (_instance == null) { _instance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cocktailmemo_db").build(); // (3) } return _instance; } public abstract CocktailmemoDAO createCocktailmemoDAO(); // (4) }
リスト7の(2)を見ると、RoomDatabaseを継承した抽象クラスになっているのがわかります。クラス名は、なんでもかまいませんが、慣習的にAppDatabaseとすることが多いです。
エンティティやDAO同様、ここでもアノテーションを記述します。それが、(1)の@Databaseです。このアノテーションのentitiesパラメータとして、このデータベースで利用するエンティティクラスを配列として指定します。ここで指定し忘れると、テーブルが作成されませんので、注意してください。versionパラメータは、データベースのバージョン番号を表します。このバージョン番号は、SQLiteDatabaseを利用した旧来のデータベース処理において、ヘルパークラス内に記述していたデータベースのバージョン番号と同様です。
このクラス内に記述する必要があるのは、本来は、DAOインターフェースを戻り値とする抽象メソッドだけです。それが、(4)です。メソッド名はなんでもかまいませんが、DAOを戻り値とする生成メソッドであることから、「create+DAOインターフェース名」とするとわかりやすいです。この抽象メソッドは、定義されているDAOインターフェース全てについて記述しておきます。
Kotlin版Room Database
本来は、この抽象メソッドを定義すればよいRoom Databaseクラスのはずなのに、リスト7では、他にさまざまなコードが記述されています。この理由については、次節で紹介するとして、先に、Kotlin版のRoom Databaseのコードを紹介します。これは、リスト8の通りです。
@Database(entities = [Cocktailmemo::class], version = 1) abstract class AppDatabase : RoomDatabase() { companion object { private var _instance: AppDatabase? = null fun getDatabase(context: Context): AppDatabase { if (_instance == null) { _instance = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "cocktailmemo_db").build() // (1) } return _instance!! } } abstract fun createCocktailmemoDAO(): CocktailmemoDAO }
アノテーションの記述方法、抽象メソッドの定義ともに、リスト7のJavaコードと考え方は同じです。