オブジェクト間の関係タイプを選択する

SQLite はリレーショナル データベースであるため、エンティティ間のリレーションを定義できます。大部分のオブジェクト リレーショナル マッピング ライブラリにおいてはエンティティ オブジェクト間の相互参照が可能ですが、Room では明示的に禁止されています。この決定の背景にある技術的な理由については、Room がオブジェクト参照をサポートしない理由を理解するをご覧ください。

関係の種類

Room は、次の関係タイプをサポートしています。

  • 1 対 1: 単一のエンティティが別の単一のエンティティに関連付けられている関係を表します。
  • 一対多: 1 つのエンティティが別のタイプの複数のエンティティに関連付けられる関係を表します。
  • 多対多: 1 つのタイプの複数のエンティティが別のタイプの複数のエンティティに関連付けられるリレーションを表します。通常、これにはジャンクション テーブルが必要です。
  • 埋め込みオブジェクトを使用したネストされた関係: エンティティが別のエンティティをプロパティとして含み、このネストされたエンティティがさらに他のエンティティを含むことができる関係を表します。これは @Embedded アノテーションを使用します。

2 つのアプローチから選択する

Room では、エンティティ間のリレーションを定義してクエリする方法が 2 つあります。次のいずれかを使用できます。

  • 埋め込みオブジェクトを含む中間データクラス
  • マルチマップの戻り値の型を持つリレーショナル クエリ関数。

中間データクラスを使用する特別な理由がない場合は、マルチマップの戻り値の型アプローチを使用することをおすすめします。このアプローチについて詳しくは、マルチマップを返すをご覧ください。

中間データクラス アプローチでは、複雑な SQL クエリを作成せずに済みますが、必要なデータクラスを追加することで、コードが複雑になる可能性もあります。つまり、マルチマップの戻り値の型アプローチでは SQL クエリで行う処理が多くなり、中間データクラス アプローチではコードで行う処理が多くなります。

中間データクラス アプローチを使用する

中間データクラス アプローチでは、Room エンティティ間のリレーションをモデル化するデータクラスを定義します。このデータクラスは、あるエンティティのインスタンスと別のエンティティのインスタンスのペアリングを埋め込みオブジェクトとして保持します。その後、このデータクラスのインスタンスがクエリ関数によって返され、アプリで使用できます。

たとえば、特定の本を借りた図書館利用者を表す UserBook データクラスを定義し、データベースから UserBook インスタンスのリストを取得するクエリ関数を定義できます。

@Dao
interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user JOIN book ON user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): LiveData<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

マルチマップの戻り値の型のアプローチを使用する

マルチマップの戻り値の型アプローチでは、追加のデータクラスを定義する必要はありません。代わりに、必要なマップ構造に基づいて関数にマルチマップの戻り値の型を定義し、エンティティ間のリレーションを SQL クエリで直接定義します。

たとえば、次のクエリ関数は、特定の本を借りた図書館利用者を表す User インスタンスと Book インスタンスのマッピングを返します。

@Query(
    """
    SELECT *
    FROM user JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNames(): Map<User, List<Book>>

マルチマップの戻り値の型を使用すると、別のエンティティを含まない 1 対 1 の関係をクエリすることもできます。次のクエリ関数は、@MapColumn アノテーションを使用して、User と借りた本の数のマッピングを返します。

@Query(
    """
    SELECT user.*, COUNT(book.id) AS book_count
    FROM user LEFT JOIN book ON user.id = book.user_id
    GROUP BY user.id
    """
)
suspend fun loadUserAndBookCount(): Map<User, @MapColumn(columnName = "book_count") Int>

埋め込みオブジェクトを作成する

オブジェクトに複数のプロパティが含まれている場合でも、データベース ロジック内では、エンティティやデータ オブジェクトを 1 つのまとまりとして表現したい場合があります。このような場合、@Embedded アノテーションを使用して、テーブル内のサブプロパティにオブジェクトを分解します。このような埋め込みプロパティは、他の列と同様にクエリを行うことができます。

たとえば、User クラスに streetcitystatepostCode プロパティの構成を表す Address プロパティを含めることができます。構成後の列をテーブル内に個別に格納するには、User クラスの Address プロパティに @Embedded アノテーションを付けます。次のコード スニペットは、この設定を示しています。

data class Address(
    val street: String?,
    val state: String?,
    val city: String?,
    @ColumnInfo(name = "post_code") val postCode: Int
)

@Entity
data class User(
    @PrimaryKey val id: Int,
    val firstName: String,
    @Embedded val address: Address?
)

User オブジェクトを表すテーブルには、idfirstNamestreetstatecitypost_code という名前の列が含まれるようになります。

1 つのエンティティに同じ型の埋め込みプロパティが複数ある場合、prefix プロパティを設定することで、各列を一意にすることができます。Room は、指定した値を埋め込みオブジェクトの各列名の先頭に追加します。