Definir relações entre objetos

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 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 de todos os objetos Playlist 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 objetos PlaylistWithSongs resultantes.
  • getSongsWithPlaylists: esse método consulta o banco de dados e retorna todos os objetos SongWithPlaylists 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.

UserWithPlaylistsAndSongs modela a relação entre as classes User (usuário) e
  PlaylistWithSongs, que, por sua vez, modela a relação entre playlist
  e músicas.

Figura 1. Diagrama de classes de relação no exemplo do app de streaming de música.

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

Vídeos

Blogs