Como o SQLite é um banco de dados relacional, é possível especificar relações entre entidades. Embora a maioria das bibliotecas de mapeamento relacional permita que objetos de entidade façam referência entre si, o Room proíbe isso explicitamente. Para saber mais sobre o raciocínio técnico por trás dessa decisão, consulte Entender por que o Room não permite referências de objetos.
Duas abordagens possíveis
No Room, existem duas maneiras de definir e consultar uma relação entre entidades: usando uma classe de dados intermediária com objetos incorporados ou um método de consulta relacional com um tipo de retorno multimapa.
Classe de dados intermediária
Na abordagem de classe de dados intermediária, uma classe de dados, que modela a relação entre as entidades do Room, é definida. Essa classe contém os pareamentos entre instâncias de uma entidade e instâncias de outra entidade como objetos incorporados. Os métodos de consulta podem retornar instâncias dessa classe de dados para uso no app.
Por exemplo, é possível definir uma classe de dados UserBook
para representar usuários de biblioteca
que pegaram livros específicos emprestados e definir um método de consulta para extrair uma lista
de instâncias UserBook
do banco de dados:
Kotlin
@Dao interface UserBookDao { @Query( "SELECT user.name AS userName, book.name AS bookName " + "FROM user, book " + "WHERE user.id = book.user_id" ) fun loadUserAndBookNames(): LiveData<List<UserBook>> } data class UserBook(val userName: String?, val bookName: String?)
Java
@Dao public interface UserBookDao { @Query("SELECT user.name AS userName, book.name AS bookName " + "FROM user, book " + "WHERE user.id = book.user_id") public LiveData<List<UserBook>> loadUserAndBookNames(); } public class UserBook { public String userName; public String bookName; }
Tipos de retorno multimapa
Na abordagem de tipo de retorno multimapa, não é necessário definir outras classes de dados. Em vez disso, defina um tipo de retorno multimapa (link em inglês) para o método com base na estrutura de mapa desejada e defina a relação entre as entidades diretamente na consulta SQL.
Por exemplo, o método de consulta abaixo retorna um mapeamento de instâncias User
e Book
para representar usuários da biblioteca que pegaram livros específicos emprestados:
Kotlin
@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) fun loadUserAndBookNames(): Map<User, List<Book>>
Java
@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) public Map<User, List<Book>> loadUserAndBookNames();
Escolher uma abordagem
O Room oferece suporte a essas duas abordagens. Assim, você pode usar aquela que funcionar melhor no seu app. Esta seção discute alguns dos motivos para preferir uma ou outra.
A abordagem de classe de dados intermediária permite evitar a criação de consultas SQL complexas, mas pode resultar em aumento da complexidade do código devido à necessidade de mais classes de dados. Em resumo, a abordagem do tipo de retorno multimapa exige que as consultas SQL executem mais tarefas, ao passo que a abordagem da classe de dados intermediária exige que o código execute mais tarefas.
Se você não tiver um motivo específico para usar classes de dados intermediárias, recomendamos a abordagem do tipo de retorno multimapa. Para saber mais sobre essa abordagem, consulte Retornar um multimapa.
O restante deste guia demonstra como definir relações usando a abordagem de classe de dados intermediária.
Criar objetos incorporados
Às vezes, é possível que você queira expressar um objeto de entidade ou de dados como
um todo coeso na lógica do banco de dados, mesmo que o objeto contenha vários
campos. Nessas situações, é possível usar a anotação
@Embedded
para representar um objeto que você gostaria de decompor como os
subcampos em uma tabela. Em seguida, pode consultar os campos incorporados da mesma forma que faria
para outras colunas individuais.
Por exemplo, a classe User
pode incluir um campo do tipo Address
que
representa uma composição de campos com os nomes street
, city
, state
e
postCode
. Para armazenar as colunas compostas separadamente na tabela, inclua um
campo Address
na classe User
que contenha a anotação @Embedded
, conforme
mostrado no snippet de código abaixo.
Kotlin
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? )
Java
public class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity public class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; }
A tabela que representa um objeto User
contém colunas com estes
nomes: id
, firstName
, street
, state
, city
e post_code
.
Se uma entidade tem vários campos incorporados do mesmo tipo, é possível manter cada
coluna única, definindo a
propriedade
prefix
. O Room adiciona o valor fornecido na propriedade ao início do nome de cada coluna
no objeto incorporado.
Definir as relações de "um para um"
Em um relação de um para um entre duas entidades, cada instância da entidade pai corresponde exatamente a uma instância da entidade filha. O inverso também é verdadeiro.
Por exemplo, considere um app de streaming de música em que o usuário tem uma biblioteca de músicas
que pertencem a ele. Cada usuário tem apenas uma biblioteca, e cada biblioteca
corresponde exatamente a um usuário. Portanto, há uma relação direta
entre a entidade User
e a entidade Library
.
Para definir uma relação de um para um, crie uma classe para cada entidade. Uma delas precisa incluir uma variável que seja uma referência à chave primária da outra.
Kotlin
@Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int ) @Entity data class Library( @PrimaryKey val libraryId: Long, val userOwnerId: Long )
Java
@Entity public class User { @PrimaryKey public long userId; public String name; public int age; } @Entity public class Library { @PrimaryKey public long libraryId; public long userOwnerId; }
Para consultar a lista de usuários e bibliotecas correspondentes, você precisa primeiro
modelar a relação de um para um entre essas duas entidades. Para fazer isso, crie uma
nova classe de dados em que cada instância contenha uma instância da entidade pai e
a instância correspondente da entidade filha. Adicione a anotação @Relation
à instância da entidade filha, com a parentColumn
definida como
o nome da coluna de chave primária da entidade pai e a entityColumn
definida com o nome da coluna da entidade filha que faz referência à chave primária
da entidade pai.
Kotlin
data class UserAndLibrary( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) val library: Library )
Java
public class UserAndLibrary { @Embedded public User user; @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) public Library library; }
Por fim, adicione um método à classe DAO que retorna todas as instâncias da classe de dados
com as entidades pai e filha pareadas. Esse método exige que
o Room execute duas consultas. Portanto, adicione a anotação @Transaction
a esse
método para garantir que toda a operação seja realizada atomicamente.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary>
Java
@Transaction @Query("SELECT * FROM User") public List<UserAndLibrary> getUsersAndLibraries();
Definir relações "um para muitos"
Uma relação de um para muitos entre duas entidades é uma relação em que cada instância da entidade pai corresponde a zero ou mais instâncias da entidade filha, mas cada instância da entidade filha só pode corresponder exatamente a uma instância da entidade pai.
No exemplo do app de streaming de música, suponha que o usuário consiga organizar
as músicas em playlists. Cada usuário pode criar quantas playlists quiser,
mas cada playlist é criada por exatamente um usuário. Portanto, há uma
relação de um para muitos entre a entidade User
e a entidade Playlist
.
Para definir uma relação de um para muitos, crie uma classe para as duas entidades. Como nas relações um para um, a entidade filha precisa incluir uma variável que seja uma referência à chave primária da entidade pai.
Kotlin
@Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int ) @Entity data class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String )
Java
@Entity public class User { @PrimaryKey public long userId; public String name; public int age; } @Entity public class Playlist { @PrimaryKey public long playlistId; public long userCreatorId; public String playlistName; }
Para consultar a lista de usuários e playlists correspondentes, é necessário
modelar a relação um para muitos entre as duas entidades. Para fazer isso, crie
uma nova classe de dados em que cada instância contenha uma instância da entidade pai e
uma lista de todas as instâncias da entidade filha correspondentes. Adicione a anotação @Relation
à instância da entidade filha, com a parentColumn
definida como
o nome da coluna de chave primária da entidade pai e a entityColumn
definida com o nome da coluna da entidade filha que faz referência à chave primária
da entidade pai.
Kotlin
data class UserWithPlaylists( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<Playlist> )
Java
public class UserWithPlaylists { @Embedded public User user; @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) public List<Playlist> playlists; }
Por fim, adicione um método à classe DAO que retorna todas as instâncias da classe de dados
com as entidades pai e filha pareadas. Esse método exige que
o Room execute duas consultas. Portanto, adicione a anotação @Transaction
a esse
método para garantir que toda a operação seja realizada atomicamente.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylists> getUsersWithPlaylists();
Definir relações de "muitos para muitos"
Em ums 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 canções nas playlists definidas pelo usuário.
Cada playlist pode incluir muitas músicas, e cada música pode fazer parte de muitas playlists
diferentes. Portanto, há uma relação de muitos para muitos
entre a entidade Playlist
e a entidade Song
.
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 relações porque geralmente não
há referência à entidade pai na entidade filha. Em vez disso, crie uma terceira
classe para representar uma entidade associativa (link em inglês), também conhecida como uma tabela de referência
cruzada, entre as duas entidades. Essa tabela precisa incluir colunas para a
chave primária de cada entidade na relação de muitos para muitos representada na
tabela. Neste exemplo, cada linha na tabela corresponde a
um par de instâncias Playlist
e Song
em que a música especificada
está incluída na playlist referenciada.
Kotlin
@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"]) data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long )
Java
@Entity public class Playlist { @PrimaryKey public long playlistId; public String playlistName; } @Entity public class Song { @PrimaryKey public long songId; public String songName; public String artist; } @Entity(primaryKeys = {"playlistId", "songId"}) public class PlaylistSongCrossRef { public long playlistId; public long songId; }
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 de todos dos objetosSong
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 de todos os objetosPlaylist
em que a música está incluída.
Nos dois casos, modele a relação entre as entidades usando a propriedade
associateBy
na anotação @Relation
em cada uma dessas
classes para identificar a entidade de referência cruzada que fornece a relação
entre as entidades Playlist
e Song
.
Kotlin
data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> ) data class SongWithPlaylists( @Embedded val song: Song, @Relation( parentColumn = "songId", entityColumn = "playlistId", associateBy = Junction(PlaylistSongCrossRef::class) ) val playlists: List<Playlist> )
Java
public class PlaylistWithSongs { @Embedded public Playlist playlist; @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = @Junction(PlaylistSongCrossref.class) ) public List<Song> songs; } public class SongWithPlaylists { @Embedded public Song song; @Relation( parentColumn = "songId", entityColumn = "playlistId", associateBy = @Junction(PlaylistSongCrossref.class) ) public List<Playlist> playlists; }
Por fim, adicione um método à classe DAO para expor a funcionalidade de consulta de que o app precisa.
getPlaylistsWithSongs
: esse método consulta o banco de dados e retorna todos os objetosPlaylistWithSongs
resultantes.getSongsWithPlaylists
: esse método consulta o banco de dados e retorna todos os objetosSongWithPlaylists
resultantes.
Ambos métodos exigem que o Room execute duas consultas. Portanto, adicione a
anotação @Transaction
aos dois métodos para garantir que toda a
operação seja realizada atomicamente.
Kotlin
@Transaction @Query("SELECT * FROM Playlist") fun getPlaylistsWithSongs(): List<PlaylistWithSongs> @Transaction @Query("SELECT * FROM Song") fun getSongsWithPlaylists(): List<SongWithPlaylists>
Java
@Transaction @Query("SELECT * FROM Playlist") public List<PlaylistWithSongs> getPlaylistsWithSongs(); @Transaction @Query("SELECT * FROM Song") public List<SongWithPlaylists> getSongsWithPlaylists();
Definir relações aninhadas
Às vezes, pode ser necessário consultar um conjunto de três ou mais tabelas relacionadas entre si. Nesse caso, é preciso definir relações aninhadas entre as tabelas.
Suponha que, no exemplo do app de streaming de música, você queira consultar todos os usuários, todas as playlists de cada usuário e todas as músicas em cada playlist de cada usuário. Usuários têm uma relação de um para muitos com playlists, e as playlists têm uma relação de muitos para muitos com as músicas. O exemplo de código abaixo mostra as classes que representam essas entidades e também a tabela de referência cruzada para a relação de muitos para muitos entre playlist e músicas:
Kotlin
@Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int ) @Entity data class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String ) @Entity data class Song( @PrimaryKey val songId: Long, val songName: String, val artist: String ) @Entity(primaryKeys = ["playlistId", "songId"]) data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long )
Java
@Entity public class User { @PrimaryKey public long userId; public String name; public int age; } @Entity public class Playlist { @PrimaryKey public long playlistId; public long userCreatorId; public String playlistName; } @Entity public class Song { @PrimaryKey public long songId; public String songName; public String artist; } @Entity(primaryKeys = {"playlistId", "songId"}) public class PlaylistSongCrossRef { public long playlistId; public long songId; }
Primeiro, modele a relação entre duas tabelas no seu conjunto como faria
normalmente, com uma classe de dados e
a anotação @Relation
. O
exemplo abaixo mostra uma classe PlaylistWithSongs
que modela uma relação de muitos para muitos
entre as classes de entidade Playlist
e Song
:
Kotlin
data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> )
Java
public class PlaylistWithSongs { @Embedded public Playlist playlist; @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef.class) ) public List<Song> songs; }
Depois de definir uma classe de dados que representa essa relação, crie outra
classe que modele a relação entre outra tabela do conjunto e
a classe de relação primária, "aninhando" a relação existente com a
nova. O exemplo abaixo mostra uma classe UserWithPlaylistsAndSongs
que modela
uma relação de um para muitos entre a classe de entidade User
e a
classe de relação PlaylistWithSongs
:
Kotlin
data class UserWithPlaylistsAndSongs( @Embedded val user: User @Relation( entity = Playlist::class, parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<PlaylistWithSongs> )
Java
public class UserWithPlaylistsAndSongs { @Embedded public User user; @Relation( entity = Playlist.class, parentColumn = "userId", entityColumn = "userCreatorId" ) public List<PlaylistWithSongs> playlists; }
A classe UserWithPlaylistsAndSongs
modela indiretamente as relações
entre as três classes de entidade: User
, Playlist
e Song
. Isso é
ilustrado na figura 1.
Se houver mais tabelas no seu conjunto, crie uma classe para modelar a relação entre cada tabela restante e a classe de relação que modela as relações entre todas as tabelas anteriores. Isso cria uma cadeia de relações aninhadas entre todas as tabelas que você quer consultar.
Por fim, adicione um método à classe DAO para expor a funcionalidade de consulta de que
o app precisa. Esse método exige que o Room execute várias consultas. Portanto, adicione a
anotação @Transaction
para
garantir que toda a operação seja realizada atomicamente:
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
Outros recursos
Para saber mais sobre como definir relações entre entidades no Room, consulte estes recursos abaixo.
Exemplos
- Android Sunflower (em inglês)
- Tivi (em inglês)
Vídeos
- O que há de novo no Room (Conferência de Desenvolvedores Android, 2019)
Blogs
- Relações do banco de dados com o Room (em inglês)