多対多のリレーションを定義してクエリを実行する

2 つのエンティティ間の多対多のリレーションとは、親エンティティの各インスタンスが子エンティティの 0 個以上のインスタンスに対応するリレーション、あるいはその逆のリレーションです。

音楽ストリーミング アプリの例で、ユーザー定義のプレイリストの曲について考えてみましょう。各プレイリストには多数の曲を含めることができ、それぞれの曲は多数のプレイリストに含められます。したがって、Playlist エンティティと Song エンティティの間には多対多のリレーションがあります。

データベースで多対多のリレーションを定義してクエリを実行する手順は次のとおりです。

  1. リレーションを定義する: 多対多のリレーションを表すエンティティと連関エンティティ(相互参照テーブル)を確立します。
  2. エンティティをクエリする: 関連エンティティをクエリする方法を決定し、目的の出力を表すデータクラスを作成します。

リレーションシップを定義する

多対多のリレーションを定義するには、まず 2 つのエンティティにそれぞれクラスを作成します。多対多のリレーションは、通常、子エンティティに親エンティティへの参照がないため、他のタイプのリレーションとは区別されます。代わりに、2 つのエンティティ間の連関エンティティ(相互参照テーブルとも呼ばれます)を表す 3 つ目のクラスを作成します。相互参照テーブルには、テーブルで表現される多対多のリレーションに含まれる各エンティティからの主キーの列が必要です。この例では、相互参照テーブルの各行は、Playlist インスタンスと Song インスタンスのペアに対応し、参照先のプレイリストに参照先の曲が含まれます。

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long,
    val playlistName: String
)

@Entity
data class Song(
    @PrimaryKey val songId: Long,
    val songName: String,
    val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"], indices = [Index("playlistId", "songId")])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

エンティティをクエリする

次のステップは、これらの連関エンティティのクエリ方法によって異なります。

  • プレイリストと各プレイリストの対応する曲のリストをクエリする場合は、1 つの Playlist オブジェクトとプレイリストに含まれる Song オブジェクトのリストを含む新しいデータクラスを作成します。
  • と各曲に対応するプレイリストのリストをクエリする場合は、1 つの Song オブジェクトと、曲を含む Playlist オブジェクトのリストを含む、新しいデータクラスを作成します。

いずれの場合も、各クラスの @Relation アノテーションの associateBy プロパティでエンティティ間のリレーションをモデル化して、Playlist エンティティと Song エンティティのリレーションを定める相互参照エンティティを特定します。

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
        parentColumns = ["playlistId"],
        entityColumns = ["songId"],
        associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

data class SongWithPlaylists(
    @Embedded val song: Song,
    @Relation(
        parentColumns = ["songId"],
        entityColumns = ["playlistId"],
        associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val playlists: List<Playlist>
)

最後に、アプリに必要なクエリ関数を公開する関数をデータアクセス オブジェクト(DAO)クラスに追加します。

getPlaylistsWithSongs
データベースにクエリを実行し、結果の PlaylistWithSongs オブジェクトをすべて返します。
getSongsWithPlaylists
データベースにクエリを実行し、結果の SongWithPlaylists オブジェクトをすべて返します。

各関数では、Room に 2 つのクエリを実行させる必要があります。両方の関数に @Transaction アノテーションを追加して、操作がアトミックに実行されるようにします。

@Transaction
@Query("SELECT * FROM Playlist")
suspend fun getPlaylistsWithSongs(): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
suspend fun getSongsWithPlaylists(): List<SongWithPlaylists>

複合キー

複合キーを使用してリレーションシップを定義する場合は、@Relation アノテーションの parentColumnsentityColumns で複数の列を指定します。

Junction で列を指定する必要がある場合は、Junction アノテーションで parentColumnsentityColumns も使用します。

次の例では、PlaylistplaylistIdcreatorId で構成される複合主キーがあります。相互参照テーブル PlaylistSongCrossRef には、プレイリストを参照するための次の列も含まれています。

@Entity(primaryKeys = ["playlistId", "creatorId"])
data class Playlist(
    val playlistId: Long,
    val creatorId: Long,
    val playlistName: String
)

@Entity
data class Song(
    @PrimaryKey val songId: Long,
    val songName: String,
    val artist: String
)

@Entity(primaryKeys = ["playlistId", "creatorId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val creatorId: Long,
    val songId: Long
)

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
        parentColumns = ["playlistId", "creatorId"],
        entityColumns = ["songId"],
        associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)