Room はプリミティブ型とボックス化型を変換する機能を備えていますが、エンティティ間のオブジェクト参照はサポートしていません。このドキュメントでは、型コンバーターの使用方法と、Room がオブジェクト参照をサポートしない理由について説明します。
型コンバータを使用する
アプリでカスタムデータ型を単一のデータベース列に保存する必要が生じることがあります。カスタム型をサポートするには、型コンバータを提供します。これは、Room が永続化できる既知の型とカスタム型との間での変換方法を Room に伝えるメソッドです。型コンバータは、@TypeConverter
アノテーションを使用して特定します。
Room データベースで Date
のインスタンスを永続化する必要があるとします。Room は Date
オブジェクトを永続化する方法を認識していないため、型コンバータを定義する必要があります。
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}
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 が認識できるようにします。
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
これらの型コンバータを定義すると、プリミティブ型を使用する場合と同様に、エンティティと DAO でカスタム型を使用できます。
@Entity
data class User(private val birthday: Date?)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE birthday = :targetDate")
fun findUsersBornOnDate(targetDate: Date): List<User>
}
@Entity
public class User {
private Date birthday;
}
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE birthday = :targetDate")
List<User> findUsersBornOnDate(Date targetDate);
}
この例では、AppDatabase
に @TypeConverters
アノテーションを付けているため、Room は定義済みの型コンバータをどこでも使用できます。ただし、@Entity
クラスまたは @Dao
クラスに @TypeConverters
アノテーションを付けることで、型コンバータを特定のエンティティまたは DAO にスコープ設定することもできます。
型コンバータの初期化を制御する
通常、Room は型コンバータのインスタンス化を処理します。ただし、場合によっては、型コンバータのクラスに追加の依存関係を渡す必要があります。つまり、型コンバータの初期化をアプリで直接制御する必要があります。その場合は、コンバータ クラスに @ProvidedTypeConverter
アノテーションを付けます。
@ProvidedTypeConverter
class ExampleConverter {
@TypeConverter
fun StringToExample(string: String?): ExampleType? {
...
}
@TypeConverter
fun ExampleToString(example: ExampleType?): String? {
...
}
}
@ProvidedTypeConverter
public class ExampleConverter {
@TypeConverter
public Example StringToExample(String string) {
...
}
@TypeConverter
public String ExampleToString(Example example) {
...
}
}
次に、@TypeConverters
でコンバータ クラスを宣言するだけでなく、RoomDatabase.Builder.addTypeConverter()
メソッドを使用してコンバータ クラスのインスタンスを RoomDatabase
ビルダーに渡します。
val db = Room.databaseBuilder(...)
.addTypeConverter(exampleConverterInstance)
.build()
AppDatabase db = Room.databaseBuilder(...)
.addTypeConverter(exampleConverterInstance)
.build();
Room がオブジェクト参照をサポートしない理由を理解する
重要ポイント: Room は、エンティティ クラス間のオブジェクト参照を許可していません。代わりに、アプリが必要とするデータを明示的にリクエストする必要があります。
データベースと各オブジェクト モデルとのリレーションをマッピングするのは一般的な方法であり、サーバーサイドでは効果的に機能します。フィールドがアクセスされたときにプログラムがフィールドを読み込む場合でも、サーバーは依然として効果的に動作します。
しかし、クライアント サイドでは、このような遅延読み込みは通常は UI スレッド上で発生するため、適しておらず、UI スレッドでディスク上の情報をクエリすると、パフォーマンスに大きな問題が発生します。通常、UI スレッドがアクティビティの更新後のレイアウトを計算して描画する際に与えられる時間は約 16 ミリ秒です。そのため、クエリに 5 ミリ秒しかかからなかったとしても、アプリによるフレーム描画が時間切れになり、明確に認識できる視覚的不具合を引き起こすことがあります。別のトランザクションが並列実行されている場合や、デバイスが別のディスク集中型タスクを実行している場合、クエリの完了にはさらに時間がかかる可能性があります。他方、遅延読み込みを利用しない場合、アプリは必要以上のデータを取得するため、メモリ消費上の問題が発生します。
通常、オブジェクト リレーショナル マッピングでは、アプリのユースケースに応じて最適化できるように、この決定をデベロッパーに任せています。デベロッパーは通常、アプリと UI の間でモデルを共有することを決定します。ただし、時間の経過とともに UI が変化していくと、デベロッパーにとって予測やデバッグが難しい問題が発生するため、この共有モデル ソリューションはあまり拡張性がありません。
たとえば、Book
オブジェクトのリストを読み込む UI について考えてみましょう。各書籍には Author
オブジェクトがあります。まず、遅延読み込みを使用して Book
のインスタンスが著者を取得するようにクエリを設計したとします。author
フィールドを最初に取得する際に、データベースをクエリします。しばらくして、アプリの UI にも著者名を表示する必要があることに気づきました。次のコード スニペットに示すように、この著者名には簡単にアクセスできます。
authorNameTextView.text = book.author.name
authorNameTextView.setText(book.getAuthor().getName());
しかし、この一見無害な変更により、メインスレッド上で Author
テーブルがクエリされることになります。
事前に著者情報をクエリした場合、そのデータが不要になったときにデータの読み込み方法を変更することが難しくなります。たとえば、アプリの UI が Author
情報を表示する必要がなくなった場合でも、表示しなくなったデータをアプリは依然として読み込み、貴重なメモリ容量を無駄にします。Author
クラスが Books
などの別のテーブルを参照する場合、アプリの効率はさらに低下します。
Room を使用して複数のエンティティを同時に参照する場合は、代わりに、各エンティティを含む POJO を作成して、対応するテーブルを結合するクエリを記述します。適切に構造化されたこのモデルと、Room の堅牢なクエリ検証機能を組み合わせることで、アプリがデータを読み込む際に使用するリソースが減り、アプリのパフォーマンスとユーザー エクスペリエンスが向上します。