Posted at

【Android Architecture Components】Room Persistence Library 和訳

More than 1 year has passed since last update.


注意

Room Persistence Libraryの和訳になります。

わかりづらい表現を意訳したり、回りくどいところを端折ったりしています。

和訳に自信がないところもあるため、間違いを見つけた場合は指摘してください。

・・・・・

Roomは、SQLiteのすべてのパワーを活用しつつ柔軟にデータベースにアクセスできるオブジェクトマッピング抽象化レイヤーを提供します。


注:Roomをアプリに導入するにはadding components to your projectを参照してください。


大量のデータを操作するアプリにとってはローカルにデータを永続させることによって大きな恩恵を受けることができます。

一般的な例では関連するデータをキャッシュすることです。

デバイスがネットワークにアクセスできな場合でも、ユーザはオフラインでそのコンテンツを閲覧することができます。

ユーザが行なった変更はデバイスがオンラインに復帰した後にサーバに同期されます。

コアフレームワークには、生のSQLコンテンツを操作するためのサポートが組み込まれています。

このAPIは強力ですが、かなり低レベルであるため、使うにはかなりの時間と労力が必要です。

・SQLクエリはコンパイル時に検証されません。スキーマが変更されると、影響を受けるSQLクエリを手動でアップデートする必要があります。これは、時間がかかり、エラーも発生しやすいプロセスです。

・SQLクエリとJavaデータオブジェクトを変換するために、たくさんのボイラープレートコードを書く必要があります。

RoomはSQLiteに抽象化レイヤーを提供し、こういった懸念に対処します。

Roomには、次の3つの主要コンセプトがあります。


Database

アノテーションを使ってエンティティのリストとデータベースのバージョンを定義するホルダークラスです。このクラスのコンテンツでは、DAO のリストが定義されています。また、このクラスは基盤となるデータベースに接続するための主なアクセスポイントでもあります。

アノテーションを付与したクラスはRoomDatabaseを継承したabstractクラスである必要があります。

実行時に、Room.databaseBuilder()または Room.inMemoryDatabaseBuilder()の呼び出しによってインスタンスを取得することができます。


Entity

データベースの1行のデータを示します。各エンティティに対して、アイテムを保持するDBのテーブルが作成されます。

エンティティクラスはDatabaseentitiesの配列を介して参照する必要があります。

@Ignoreアノテーションを付けない限り、エンティティの各フィールドはデータベースに保持されます。


注:エンティティは空のコンストラクタ(DAOクラスが各永続化フィールドにアクセスできる場合)またはエンティティのフィールドの型と名前に一致する引数を持つコンストラクタを持つことができます。Roomは、完全または、一部のフィールドのみを受け取るコンストラクタを持つことができます。



DAO

DAOはRoomのメインコンポーネントでDBにアクセスするメソッドを定義する役割があります。@Databaseを付与したクラスは引数無しで、@Daoが付与されたクラスを返す抽象メソッドを定義する必要があります。

Roomはコンパイル時にこのクラスの実装を行います。


注:クエリビルダーまたはダイレクトクエリの代わりにDAOクラスを使用してDBにアクセスすることで、DBアーキテクチャのさまざまなコンポーネントを分離することができます。

さらにDAOを使用すると、アプリのテスト時に簡単にDBアクセスをモック化することができます。


これらのコンポーネントとアプリ外部との関係を下記の図に示します。

room_architecture.png

次のコードスニペットには、エンティティ1つ、DAOを1つ持つDBのサンプルです。


User.java

@Entity

public class User {
@PrimaryKey
private int uid;

@ColumnInfo(name = "first_name")
private String firstName;

@ColumnInfo(name = "last_name")
private String lastName;

// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}



UserDao.java

@Dao

public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);

@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);

@Insert
void insertAll(User... users);

@Delete
void delete(User user);
}



AppDatabase.java

@Database(entities = {User.class}, version = 1)

public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

上記のファイル作成後、次のようにして作成されたDBのインスタンスを取得します。

AppDatabase db = Room.databaseBuilder(getApplicationContext(),

AppDatabase.class, "database-name").build();


注:AppDatabaseオブジェクトをインスタンス化するときは、各RoomDatabaseインスタンスのコストが高く、複数のインスタンスにアクセスする必要がほとんどないため、シングルトン設計パターンを用いてください。



エンティティ

クラスに@Entityアノテーションが付与され、@Databaseアノテーションのentitiesが参照されると、RoomはそのエンティティのテーブルをDBに作成します。

デフォルトでは、Roomはエンティティに定義された各フィールドのカラムを作成します。

もし永続化したくないフィールドがある場合は、以下のように@Ignoreを付けます。

@Entity

class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

フィールドを永続化するために、Roomはフィールドにアクセスする必要があります。

そのため、フィールドをpublicまたはフィールドのセッター、ゲッターを定義します。

セッター、ゲッターメソッドを使用する場合は、Java Beansの規則に従ってください。


プライマリキー

各エンティティは少なくとも1つのプライマリキーとするフィールドを定義する必要があります。

エンティティ内にフィールドが1つの場合でも@PrimaryKeyは必要です。

また、エンティティに自動でIDを割り振りたい場合は@PrimaryKeyautoGenerateをセットします。

もし、複合プライマリキーをエンティティにある場合は下記のコードのように、@EntityアノテーションのprimaryKeys

@Entity(primaryKeys = {"firstName", "lastName"})

class User {
public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

デフォルトでは、Roomはクラス名をDBのテーブル名として使います。

もし、別の名前にしたい場合は下記のコードのように@EntityアノテーションのtableNameをセットします。

@Entity(tableName = "users")

class User {
...
}


注意:SQLiteのテーブル名は大文字と小文字を区別しません。


tableNameと同様に、Roomはフィールド名をDBのカラム名として使用します。

もし、別の名前にしたい場合は下記のように@ColumnInfoを付与します。

@Entity(tableName = "users")

class User {
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}


インデックスとユニーク

データのアクセス方法によっては、クエリを高速化するためにDBの特定のフィールドにインデックスをつけることができます。

エンティティのインデックスを追加するには@Entityアノテーションにindicesプロパティを含め、インデックスまたは複合インデックスに含めるカラム名を

リストします。

class User {

@PrimaryKey
public int id;

public String firstName;
public String address;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}

場合によっては、DB内の特定のフィールドまたはフィールドグループが一意(ユニーク)である必要があります。

@Indexuniqueプロパティをtrueにすることで一意プロパティを強制することができます。

次のサンプルでは、テーブルに同じfirstNamelastNameの行を2行持たせないようにします。

@Entity(indices = {@Index(value = {"first_name", "last_name"},

unique = true)})
class User {
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}


リレーションシップ

SQLiteはリレーショナルデータベースなので、オブジェクト間のリレーションシップを指定できます。 ほとんどのORMライブラリは、エンティティオブジェクトが互いに参照することを許可していますが、Roomはこれを明示的に禁止しています。 詳細については、Appendix:エンティティ間のオブジェクト参照の禁止を参照してください。

直接リレーションシップを使用することはできませんが、Roomではエンティティ間の外部キー制約を定義することができます。

たとえばBookという別のエンティティがある場合、次のコードのように@ForeignKeyアノテーションを使用してUserエンティティとのリレーションを定義できます。

@Entity(foreignKeys = @ForeignKey(entity = User.class,

parentColumns = "id",
childColumns = "user_id"))
class Book {
@PrimaryKey
public int bookId;

public String title;

@ColumnInfo(name = "user_id")
public int userId;
}

外部キーは非常に強力です。

参照されるエンティティが更新されたときに何をするかを指定することができます。

たとえば、@ForeignKeyアノテーションにonDelete=CASCADEを含めることによって、対応するUserのインスタンスが削除された場合、SQLiteにユーザーのすべてのブックを削除するよう指示できます。


注:SQLiteは@Insert(OnConflict=REPLACE)を、単一のUPDATE操作ではなく、REMOVEおよびREPLACE操作のセットとして処理します。 競合する値を置き換えるこの方法は、外部キー制約に影響する可能性があります。 詳細については、ON_CONFLICT句のSQLiteドキュメントを参照してください。



ネストされたオブジェクト

時には、少ないフィールドのオブジェクトでもエンティティまたはPOJOをDBロジック内にまとまったものとして表現したいことがあります。

この場合、@Embeddedアノテーションを使用して、テーブル内のサブフィールドのオブジェクトとして示すことができます。

他のカラムと同様にこのフィールドをクエリすることができます。

たとえば、Userクラスには、streetcitystatepostCodeという名前のフィールドを持つ構成のAddress型のフィールドを含めることができます。 作成された各カラムをテーブルに格納するには、次のコードに示すように、@EmbeddedでアノテーションされたAddressフィールドをUserクラスに追加します。

class Address {

public String street;
public String state;
public String city;

@ColumnInfo(name = "post_code")
public int postCode;
}

@Entity
class User {
@PrimaryKey
public int id;

public String firstName;

@Embedded
public Address address;
}

User オブジェクトを表すテーブルには、idfirstNamestreetstatecity、およびpost_codeという名前のカラムが含まれています。


注:埋め込みフィールドには、他の埋め込みフィールドを含めることができます。


エンティティ内に複数の同じ型の埋め込みフィールドを持つ場合は、prefixプロパティを設定してい各カラムを一意に保つことができます。


DAO(Data Access Objects)

DAOはRoomの主要なコンポーネントです。DAOはクリーンな方法でDBへのアクセスを抽象化します。


注:RoomはUIを長時間ロックしてしまう可能性があるので、ビルダーでallowMainThreadQueries()を呼ばない限りMainスレッドでDBにアクセスできません。

非同期クエリ(LiveDataまたはRxJava Flowableを返すクエリ)は、必要に応じてバックグラウンドスレッドで非同期にクエリを実行するため、このルールは除外されています。



便利なメソッド

DAOクラスを使用して表現できる複数の便利なクエリがあります。

このドキュメントには、いくつかの一般的な例が含まれています。


Insert

DAOメソッドを作成して@Insertで注釈を付けると、Roomは単一のトランザクションですべての引数をデータベースに挿入する実装を生成します。

いくつかの例を以下に示します。

@Dao

public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);

@Insert
public void insertBothUsers(User user1, User user2);

@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}

@Insertメソッドの引数が1つの場合、インサートしたアイテムのrowIdlogn型で返すことができます。

もし引数が配列かコレクションクの場合は、代わりにlong[]またはList<Long>を返します。

詳細については、@Insertアノテーションのリファレンスドキュメント、およびROWIDテーブルのSQLiteドキュメントを参照してください。


Update

Updateは引数でエンティティのセットを渡してDBを更新するための便利なメソッドです。

各エンティティのプライマリキーに対応するクエリを使用します。

以下に定義例を示します。

@Dao

public interface MyDao {
@Update
public void updateUsers(User... users);
}

必須ではありませんが、上記のメソッドの代わりにintを返すメソッドを定義すると更新した行数を返します。


Delete

Deleteは引数でエンティティのセットを渡してDBから行を削除するための便利なメソッドです。

行を削除するためにプライマリキーを使用します。

以下に定義例を示します。

@Dao

public interface MyDao {
@Delete
public void deleteUsers(User... users);
}

必須ではありませんが、上記のメソッドの代わりにintを返すメソッドを定義すると削除した行数を返します。


@Query

@QueryはDAOクラスで使用する主要なアノテーションです。

DBに対して読み込み/書き込みの操作を許可します。

@Queryメソッドはコンパイル時に検証されるため、クエリに問題がある場合は、実行時エラーの代わりにコンパイルエラーが発生します。

Roomは戻り値の検証も行います。

クエリで照会したカラム名とオブジェクトのフィールド名が一致しない場合、下記のいずれかのアラートを出します。

・フィールド名の一部のみが一致している場合は警告が表示されます。

・フィールド名が一致しない場合は、エラーが発生します。


シンプルなクエリ

@Dao

public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}

これはすべてのユーザを取得するシンプルなクエリです。クエリに構文エラーがある場合、またはユーザテーブルがDBに存在しない場合は、Roomはアプリコンパイル時に適切なエラーメッセージを表示します。


クエリにパラメータを渡す

多くの場合、特定の年齢よりも古いユーザーのみを表示するなど、フィルタリング操作を実行するためにパラメータをクエリに渡す必要があります。

これを実現するために、以下のコードのようにメソッドの引数をRoomのアノテーションに含めるようにします。

@Dao

public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}

コンパイル時にこのクエリが処理されると、Roomは:minAgeバインドパラメータとメソッドの引数のminAgeを照合します。

もし不一致がある場合はコンパイル時にエラーが発生します。

また以下のように複数のパラーメータを渡したり、複数回参照することができます。

@Dao

public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}


一部のカラムを返す

多くの場合、エンティティのいくつかのフィールドだけを取得する必要があります。

たとえば、ユーザの詳細を表示するのではなく、ユーザの姓と名だけをUIに表示される場合があります。 アプリのUIに表示するカラムのみを取得することで、貴重なリソースを節約でき、クエリがより高速に完了します。

Roomではクエリから返された結果をオブジェクトに変換できれば、任意のJavaオブジェクトで結果のカラムを受け取ることができます。

たとえば、次のPOJOを作成してユーザーの名と姓を取得できます。

public class NameTuple {

@ColumnInfo(name="first_name")
public String firstName;

@ColumnInfo(name="last_name")
public String lastName;
}

このPOJOをクエリメソッド内で使用することができます。

@Dao

public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}

Roomは、クエリがfirst_name列とlast_name列の値を返し、これらの値をNameTupleクラスのフィールドにマッピングできることを知っているので、Roomは適切なコードを生成することができます。

もし、カラムが多かったり、NameTupleクラスに存在しないカラムを返す場合は警告を表示します。


注:POJOは同様に@Embeddedアノテーションを使用できます。



引数にコレクションを渡す

クエリの中には、実行時までパラメータの正確な数がわからない、可変数のパラメータを渡す必要があるものがあります。 たとえば、ある地域データに一致するユーザー情報を取得する場合など。

Roomは、実行時にパラメータがコレクションであることを理解して自動的に数に応じて展開します。

@Dao

public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}


クエリの監視

クエリが実装されて、データに変更があった場合にUIが自動で更新したい場合があると思います。

これを実現するには、クエリメソッドの返り値にLiveDataを使用します。

Roomは、DBが更新されるとLiveDataの更新に必要なコードを生成します。

@Dao

public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}


注:バージョン1.0では、Roomはクエリでアクセスされたテーブルのリストを使用して、LiveDataオブジェクトを更新するかどうかを決定します。



RxJava

また、定義したクエリから、RxJava2のPublisherFlowableオブジェクトを返すこともできます。

この機能を使用するには、Roomグループのandroid.arch.persistence.room:rxjava2アプリのビルドGradleに追加します。 次のコードスニペットに示すように、RxJava2で定義された型のオブジェクトを返すことができます。

@Dao

public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}


カーソル

直接、行へのアクセスが必要な場合は、下記のようにしてクエリからCursorを返すことができます。

@Dao

public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}


注意:Cursor APIを使用すると、行が存在するかどうか、または行に含まれる値が保証されないため、使用することをお勧めしません。 この機能は、すでにカーソルが必要なコードがあり、簡単にリファクタリングできないコードがある場合にのみ使用してください。



複数のテーブルに問い合わせる

クエリの中には、結果を出すために複数のテーブルにアクセスする必要があるものがあります。

Roomは任意のクエリを書くことができるので、テーブルを結合することもできます。

さらに、レスポンスがFlowableLiveDataなどの観測可能なデータ型である場合、Roomは更新があった時のためにクエリで参照されるすべてのテーブルを監視します。

次のコードスニペットでは、貸出をしているユーザーを含むテーブルと、現在貸出中の書籍に関するデータを含むテーブルの間で、テーブル結合を実行して情報を統合する方法を示します。

@Dao

public interface MyDao {
@Query("SELECT * FROM book "
+ "INNER JOIN loan ON loan.book_id = book.id "
+ "INNER JOIN user ON user.id = loan.user_id "
+ "WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}

これらのクエリはでPOJOを返すことができます。

例えば、ユーザ名とペットの名前を取得する場合はこのようになります。

@Dao

public interface MyDao {
@Query("SELECT user.name AS userName, pet.name AS petName "
+ "FROM user, pet "
+ "WHERE user.id = pet.user_id")
public LiveData<List<UserPet>> loadUserAndPetNames();

// You can also define this class in a separate file, as long as you add the
// "public" access modifier.
static class UserPet {
public String userName;
public String petName;
}
}


TypeConverterを使う

Roomは、プリミティブ型とそのラッパークラスをサポートしています。

一つのカラムに独自の型のデータを使用したい場合があります。

このような独自の型をサポートするには、Roomが扱うことのできる型に変換するTypeConverterを使用します。

例えば、Dateのインスタンスを永続化したい場合は、次のようにTypeConverterを記述して同等のUnixタイムスタンプをDBに格納することができます。

public class Converters {

@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}

@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}

上記の例では、DateオブジェクトをLongオブジェクトに変換するメソッドと、LongからDateへの逆変換を実行する2つのメソッドを定義しています。 RoomはLongオブジェクトに対応しているので、このコンバータを使用してDate型の値を保持することができます。

次に、@TypeConvertersアノテーションをAppDatabaseクラスに追加して、RoomがそのAppDatabaseの各エンティティとDAOに対して定義したコンバータを使用できるようにします。


AppDatabase.java

@Database(entities = {User.java}, version = 1)

@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

これらのコンバータを使用すると、次のコードスニペットに示すように、プリミティブ型を使用するのと同じように、他のクエリで独自の型を使用できます。


User.java

@Entity

public class User {
...
private Date birthday;
}


UserDao.java

@Dao

public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}

@TypeConvertersをエンティティ、DAO、DAOメソッドを含む異なるスコープごとに制限することができます。

詳細については、@TypeConvertersアノテーションのドキュメントを参照してください。


マイグレーション

アプリの機能を追加したり変更したりすると、エンティティクラスも変更する必要がある場合があります。

ユーザーがアプリを更新し、リモートサーバーからデータを回復できない場合に、既存のすべてのデータを失わないようにします。

Roomでは、次の方法でユーザデータを保護するMigrationクラスを生成することができます。

MigrationクラスはstartVersionendVersionを指定します。

実行時にRoomはそれぞれのMigrationクラスのmigrate()メソッドを実行し、正しい順序でデータベースを新しいバージョンにマイグレーションします。


注意:適切にマイグレーションを行わないと、Roomはデータベースを再構築します。つまり、データベース内のすべてのデータが失われます。


Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")

.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};


注意:マイグレーションロジックを期待どおりに機能させるには、クエリを定義した定数を参照するのではなく、完全クエリを使用します。


マイグレーション処理が完了すると、Roomはスキーマを検証して、マイグレーションが正しく行われたかを確認します。

Roomが問題を検出した場合、不一致情報を含む例外がスローされます。


マイグレーションテスト

マイグレーションは正しく実装されていないとクラッシュループが発生する可能性があります。

アプリの安定性を維持するために、事前にマイグレーションテストをする必要があります。

こてテストを支援するMavenアーティファクトを提供しています。

このアーティファクトではDBのスキーマをエクスポートする必要があります。


スキーマのエクスポート

コンパイル時に、Roomはデータベースのスキーマ情報をJSONファイルにエクスポートします。 スキーマをエクスポートするには、build.gradleファイルのroom.schemaLocationアノテーションプロセッサプロパティを設定します。


build.gradle

android {

...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}

データベースのスキーマ履歴を表すJSONファイルは、バージョン管理システムに保存する必要があります。これにより、Roomはテスト目的で古いバージョンのデータベースを作成できます。

これらのマイグレーションテストをするには、RoomのMavenアーティファクトandroid.arch.persistence.room:testingをテストの依存関係に追加し、次のコードスニペットに示すように、スキーマの場所をアセットフォルダとして追加します。


build.gradle

android {

...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}

テストパッケージには、これらのスキーマファイルを読み取ることができるMigrationTestHelperクラスが用意されています。 また、Junit4のTestRuleクラスであるため、作成されたデータベースを管理できます。

マイグレーションテストのサンプルは以下のようになります。

@RunWith(AndroidJUnit4.class)

public class MigrationTest {
private static final String TEST_DB = "migration-test";

@Rule
public MigrationTestHelper helper;

public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}

@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

// db has schema version 1. insert some data using SQL queries.
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);

// Prepare for the next version.
db.close();

// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);

// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}


データベースのテスト

Roomではテスト時にデータアクセスのレイヤーのモックを簡単に作ることができます。

DBをテストするには2つの方法があります。


  • 開発用PCでテストする

  • Android端末でテストする


開発用PCでテストする

RoomはSQLite Support Libraryを使用します。

これは、Android Frameworkクラスのインターフェースと一致するインターフェースを提供します。

このサポートにより、サポートライブラリのカスタム実装を渡してデータベースクエリをテストすることができます。

この設定ではテストが高速に実行できますが、デバイス上で実行されているSQLiteのバージョンとユーザーのデバイスがホストマシン上のバージョンと一致しない可能性があるため、おすすめできません。


Android端末でテストする

データベースの実装をテストするために推奨される方法は、Android端末で実行されるJUnitテストを作成することです。

このテストはActivityの作成を必要としないため、UIテストよりも早くに行うことができます。

外部影響を受けないように、下記のサンプルのようにしてメモリ内にデータベースを作成してテストします。

@RunWith(AndroidJUnit4.class)

public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;

@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}

@After
public void closeDb() throws IOException {
mDb.close();
}

@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}

データベースのマイグレーションテストについては、マイグレーションテストを参照してください。


Appendix:エンティティ間のオブジェクト参照の禁止

DBからそれぞれのモデルオブジェクトにマッピングすることは一般的で、サーバ側ではアクセスされたフィールドをロードする遅延ロードが効率的に機能します。

しかし、クライアント側ではUIスレッド上でディスクに保存してある情報を参照するため、パフォーマンス上の大きな問題から、遅延ロードは実装できません。

UIスレッドは、更新されたレイアウトの計算をして描画するために約16msかかるため、クエリがわずか5msだとしても、アプリがフレームを描画するために時間かかりすぎて、UIのフリーズが顕著に発生する可能性があります。

さらに悪いことに、別のトランザクションが並行して実行されている場合や、デバイスがディスクに負担がかかる重いタスクでビジー状態になっている場合には、クエリの完了にさらに時間がかかる可能性があります。

しかし、遅延ロードを使用しない場合、アプリは必要以上のデータを取得し、メモリを圧迫する問題を引き起こします。

ORMは通常この決定をアプリのユースケースに合わせてどうするかを開発者に委ねています。

しかし残念なことに、開発者は結局のところアプリとUIでモデルを共有してしまいます。

UIを変更するにつれて、デバッグが困難になる問題が発生します。

たとえば、BookオブジェクトのリストをロードするUIを作成し、各BookにはAuthorオブジェクトを持つとします。 最初の設計では、BookのインスタンスがgetAuthor()メソッドを使用してAuthorを返すように、遅延ロードを使用するようにクエリを設計するかもしれません。getAuthor()の最初の呼び出しは、データベースに照会します。 後で、あなたはUIに作者の名前を表示する必要があることに気がつきます。

この場合、次のコードスニペットに示すように、メソッド呼び出しを簡単に追加できます。

authorNameTextView.setText(user.getAuthor().getName());

しかし、上記の変更はメインスレッドでAuthorテーブルを照会する原因となります。

多くの箇所でこの実装を行い、Authorを表示する必要がなくなった場合、Authorを取得する実装を変更することを難しくし、アプリはこの表示されなくなったAuthorのデータを引き続き読み込むことになります。

AuthorクラスがgetBooks()などで、別のテーブルを参照すると、この状況はさらに悪化します。

これらの理由から、Roomはエンティティクラス間のオブジェクト参照を許可しません。 代わりに、アプリが必要とするデータを明示的に要求する必要があります。