Definir e consultar relações de muitos para muitos

Em uma relação de muitos para muitos entre duas entidades, cada instância da entidade pai corresponde a zero ou mais instâncias da entidade filha. O inverso também é verdadeiro.

No exemplo do app de streaming de música, considere as músicas nas playlists definidas pelo usuário. Cada playlist pode incluir muitas músicas, e cada música pode fazer parte de muitas playlists. Portanto, há uma relação de muitos para muitos entre as entidades Playlist e Song.

Siga estas etapas para definir e consultar relações de muitos para muitos no seu banco de dados:

  1. Defina a relação: estabeleça as entidades e a entidade associativa, ou tabela de referência cruzada, para representar a relação de muitos para muitos.
  2. Consulte as entidades: determine como você quer consultar as entidades relacionadas e crie classes de dados para representar a saída pretendida.

Definir a relação

Para definir uma relação de muitos para muitos, crie uma classe para cada entidade. As relações de muitos para muitos são diferentes de outros tipos de relacionamento porque geralmente não há referência à entidade pai na entidade filha. Em vez disso, crie uma terceira classe para representar uma entidade associativa, ou tabela de referência cruzada, entre as duas entidades. A tabela de referência cruzada precisa ter colunas para a chave primária de cada entidade no relacionamento de muitos para muitos representado na tabela. Neste exemplo, cada linha na tabela de referência cruzada corresponde a um par de uma instância Playlist e uma instância Song em que a playlist referenciada inclui a música referenciada.

@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
)

Consultar as entidades

A próxima etapa depende de como você quer consultar as entidades relacionadas.

  • Caso queira consultar playlists e uma lista das músicas correspondentes em cada playlist, crie uma nova classe de dados que contenha um único objeto Playlist e uma lista dos objetos Song que a playlist inclui.
  • Caso queira consultar músicas e uma lista das playlists correspondentes para cada música, crie uma nova classe de dados que contenha um único objeto Song e uma lista dos objetos Playlist que incluem a música.

Nos dois casos, modele o relacionamento entre as entidades usando a associateBy propriedade na @Relation anotação em cada uma dessas classes para identificar a entidade de referência cruzada que fornece a relação entre a entidade Playlist e a entidade 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>
)

Por fim, adicione uma função à classe de objeto de acesso a dados (DAO, na sigla em inglês) para expor a função de consulta que seu app precisa.

getPlaylistsWithSongs
Consulta o banco de dados e retorna todos os objetos PlaylistWithSongs resultantes.
getSongsWithPlaylists
Consulta o banco de dados e retorna todos os objetos SongWithPlaylists resultantes.

Cada função exige que o Room execute duas consultas. Adicione a @Transaction anotação às duas funções para garantir que a operação seja executada atomicamente.

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

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

Chaves compostas

Se você definir a relação usando chaves compostas, especifique várias colunas em parentColumns e entityColumns da anotação @Relation.

Se você precisar especificar colunas na Junction, use parentColumns e entityColumns na anotação Junction também.

No exemplo a seguir, Playlist tem uma chave primária composta por playlistId e creatorId. A tabela de referência cruzada PlaylistSongCrossRef também inclui essas colunas para referenciar a playlist.

@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>
)