Room を使用して複雑なデータを参照する

Room はプリミティブ型とボックス化型を変換する機能を備えていますが、エンティティ間のオブジェクト参照はサポートしていません。このドキュメントでは、型コンバーターの使用方法と、Room がオブジェクト参照をサポートしない理由について説明します。

型コンバータを使用する

アプリでカスタムデータ型を単一のデータベース列に保存する必要が生じることがあります。カスタム型をサポートするには、型コンバータを提供します。これは、Room が永続化できる既知の型とカスタム型との間での変換方法を Room に伝えるメソッドです。型コンバータは、@TypeConverter アノテーションを使用して特定します。

Room データベースで Date のインスタンスを永続化する必要があるとします。Room は Date オブジェクトを永続化する方法を認識していないため、型コンバータを定義する必要があります。

Kotlin

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

Java

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 が認識できるようにします。

Kotlin

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

これらの型コンバータを定義すると、プリミティブ型を使用する場合と同様に、エンティティと DAO でカスタム型を使用できます。

Kotlin

@Entity
data class User(private val birthday: Date?)

@Dao
interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  fun findUsersBornOnDate(targetDate: Date): List<User>
}

Java

@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 アノテーションを付けます。

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

次に、@TypeConverters でコンバータ クラスを宣言するだけでなく、RoomDatabase.Builder.addTypeConverter() メソッドを使用してコンバータ クラスのインスタンスを RoomDatabase ビルダーに渡します。

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

Room がオブジェクト参照をサポートしない理由を理解する

重要ポイント: Room は、エンティティ クラス間のオブジェクト参照を許可していません。代わりに、アプリが必要とするデータを明示的にリクエストする必要があります。

データベースと各オブジェクト モデルとのリレーションをマッピングするのは一般的な方法であり、サーバーサイドでは効果的に機能します。フィールドがアクセスされたときにプログラムがフィールドを読み込む場合でも、サーバーは依然として効果的に動作します。

しかし、クライアント サイドでは、このような遅延読み込みは通常は UI スレッド上で発生するため、適しておらず、UI スレッドでディスク上の情報をクエリすると、パフォーマンスに大きな問題が発生します。通常、UI スレッドがアクティビティの更新後のレイアウトを計算して描画する際に与えられる時間は約 16 ミリ秒です。そのため、クエリに 5 ミリ秒しかかからなかったとしても、アプリによるフレーム描画が時間切れになり、明確に認識できる視覚的不具合を引き起こすことがあります。別のトランザクションが並列実行されている場合や、デバイスが別のディスク集中型タスクを実行している場合、クエリの完了にはさらに時間がかかる可能性があります。他方、遅延読み込みを利用しない場合、アプリは必要以上のデータを取得するため、メモリ消費上の問題が発生します。

通常、オブジェクト リレーショナル マッピングでは、アプリのユースケースに応じて最適化できるように、この決定をデベロッパーに任せています。デベロッパーは通常、アプリと UI の間でモデルを共有することを決定します。ただし、時間の経過とともに UI が変化していくと、デベロッパーにとって予測やデバッグが難しい問題が発生するため、この共有モデル ソリューションはあまり拡張性がありません。

たとえば、Book オブジェクトのリストを読み込む UI について考えてみましょう。各書籍には Author オブジェクトがあります。まず、遅延読み込みを使用して Book のインスタンスが著者を取得するようにクエリを設計したとします。author フィールドを最初に取得する際に、データベースをクエリします。しばらくして、アプリの UI にも著者名を表示する必要があることに気づきました。次のコード スニペットに示すように、この著者名には簡単にアクセスできます。

Kotlin

authorNameTextView.text = book.author.name

Java

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

しかし、この一見無害な変更により、メインスレッド上で Author テーブルがクエリされることになります。

事前に著者情報をクエリした場合、そのデータが不要になったときにデータの読み込み方法を変更することが難しくなります。たとえば、アプリの UI が Author 情報を表示する必要がなくなった場合でも、表示しなくなったデータをアプリは依然として読み込み、貴重なメモリ容量を無駄にします。Author クラスが Books などの別のテーブルを参照する場合、アプリの効率はさらに低下します。

Room を使用して複数のエンティティを同時に参照する場合は、代わりに、各エンティティを含む POJO を作成して、対応するテーブルを結合するクエリを記述します。適切に構造化されたこのモデルと、Room の堅牢なクエリ検証機能を組み合わせることで、アプリがデータを読み込む際に使用するリソースが減り、アプリのパフォーマンスとユーザー エクスペリエンスが向上します。