Как определять и запрашивать отношения "многие ко многим"

Связь "многие ко многим" между двумя объектами – это связь, при которой каждый экземпляр родительского объекта соответствует нулю или нескольким экземплярам дочернего объекта, и наоборот.

В примере с музыкальным потоковым сервисом учитываются песни в плейлистах, созданных пользователями. В каждый плейлист можно добавить много треков, и каждый трек может быть в нескольких плейлистах. Таким образом, между объектами Playlist и Song существует связь "многие ко многим".

Чтобы определить и запросить отношения "многие ко многим" в базе данных, выполните следующие действия:

  1. Определите связь. Установите сущности и ассоциативную сущность или таблицу перекрестных ссылок, чтобы представить связь "многие ко многим".
  2. Запрос к объектам. Определите, как вы хотите запрашивать связанные объекты, и создайте классы данных, представляющие нужный результат.

Как определить связь

Чтобы определить связь "многие ко многим", сначала создайте класс для каждого из двух объектов. Связи "многие ко многим" отличаются от других типов связей тем, что в дочернем объекте обычно нет ссылки на родительский объект. Вместо этого создайте третий класс, который будет представлять ассоциативный объект или перекрестную таблицу между двумя объектами. В таблице перекрестных ссылок должны быть столбцы для первичного ключа каждого объекта, связанного отношением "многие ко многим". В этом примере каждая строка в таблице перекрестных ссылок соответствует паре экземпляров 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
)

Как запрашивать объекты

Дальнейшие действия зависят от того, как вы хотите запрашивать эти связанные объекты.

  • Если вы хотите запросить плейлисты и список соответствующих песен для каждого плейлиста, создайте новый класс данных, который содержит один объект Playlist и список объектов Song, включенных в плейлист.
  • Если вы хотите запросить песни и список соответствующих плейлистов для каждой песни, создайте новый класс данных, который содержит один объект Song и список объектов Playlist, включающих песню.

В любом случае смоделируйте связь между элементами, используя свойство associateBy в аннотации @Relation в каждом из этих классов, чтобы определить перекрестную ссылку на элемент, который устанавливает связь между элементами 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 требуется выполнить два запроса. Добавьте аннотацию @Transaction к обеим функциям, чтобы обеспечить атомарное выполнение операции.

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

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

Составные ключи

Если вы определяете связь с помощью составных ключей, укажите несколько столбцов в тегах parentColumns и entityColumns аннотации @Relation.

Если вам нужно указать столбцы в Junction, используйте parentColumns и entityColumns в аннотации Junction.

В примере ниже у таблицы Playlist составной первичный ключ, состоящий из столбцов playlistId и creatorId. В таблице перекрестных ссылок 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>
)