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

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

型コンバーターを使用する

アプリによっては、カスタムデータ型を使用し、その値を単一のデータベース列に格納することが必要になる場合があります。この種のカスタム型のサポートを追加するには、TypeConverter を指定します。これは、カスタムクラスと Room が永続化できる既知のタイプとの間で変換を行います。

たとえば、Date のインスタンスを永続化する場合、次のような TypeConverter を記述することで、同等の Unix タイムスタンプをデータベース内に格納できます。

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();
        }
    }
    

上記の例は、2 つの関数を定義しています。1 つは、Date オブジェクトを Long オブジェクトに変換する関数で、もう 1 つは、Long から Date への逆変換を実行する関数です。Room は Long オブジェクトを永続化する方法をすでに知っているため、このコンバーターを使用して Date 型の値を永続化できます。

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

AppDatabase

Kotlin

    @Database(entities = arrayOf(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();
    }
    

このようなコンバーターを利用することで、プリミティブ型を使用する場合と同じように、別のクエリ内でカスタム型を使用できるようになります。次のコード スニペットをご覧ください。

User

Kotlin

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

Java

    @Entity
    public class User {
        private Date birthday;
    }
    

UserDao

Kotlin

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

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 がオブジェクト参照をサポートしない理由を理解する

重要ポイント: 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 の堅牢なクエリ検証機能を組み合わせることで、アプリがデータをロードする際に使用するリソースが減り、アプリのパフォーマンスとユーザー エクスペリエンスが向上します。