Escolher tipos de relacionamento 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.

Tipos de relações

O Room oferece suporte aos seguintes tipos de relações:

  • Um para um: representa uma relação em que uma única entidade está relacionada a outra entidade.
  • Um para muitos: representa uma relação em que uma única entidade pode estar relacionada a várias entidades de outro tipo.
  • Muitos para muitos: representa uma relação em que várias entidades de um tipo podem estar relacionadas a várias entidades de outro tipo. Isso geralmente exige uma tabela de junção.
  • Relações aninhadas usando objetos incorporados: representa uma relação em que uma entidade contém outra entidade como uma propriedade, e essa entidade aninhada pode conter outras entidades. Isso usa a anotação @Embedded.

Escolher entre duas abordagens

No Room, existem duas maneiras de definir e consultar uma relação entre entidades. É possível usar:

  • Uma classe de dados intermediária com objetos incorporados ou
  • Uma função de consulta relacional com um tipo de retorno multimapa.

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.

A abordagem de classe de dados intermediária permite evitar a criação de consultas SQL complexas, mas também pode resultar em aumento da complexidade do código porque exige 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.

Usar a abordagem de 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. As funções 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 uma função de consulta para extrair uma lista de instâncias UserBook do banco de dados:

@Dao
interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user JOIN book ON user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): LiveData<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

Usar a abordagem de tipo 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 para a função com base na estrutura de mapa desejada e defina a relação entre as entidades diretamente na consulta SQL.

Por exemplo, a função de consulta abaixo retorna um mapeamento de instâncias User e Book para representar usuários da biblioteca que pegaram livros específicos emprestados:

@Query(
    """
    SELECT *
    FROM user JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNames(): Map<User, List<Book>>

Com os tipos de retorno multimapa, também é possível consultar relações um para um que não envolvem outra entidade. A função de consulta abaixo retorna um mapeamento de User e o número de livros que eles pegaram emprestados usando a @MapColumn anotação:

@Query(
    """
    SELECT user.*, COUNT(book.id) AS book_count
    FROM user LEFT JOIN book ON user.id = book.user_id
    GROUP BY user.id
    """
)
suspend fun loadUserAndBookCount(): Map<User, @MapColumn(columnName = "book_count") Int>

Criar objetos incorporados

Às vezes, você quer expressar uma entidade ou um objeto de dados como um todo coeso na lógica do banco de dados, mesmo que o objeto tenha várias propriedades. Nessas situações, use a @Embedded anotação para decompor um objeto nas subpropriedades em uma tabela. Em seguida, é possível consultar as propriedades incorporadas da mesma forma que faz para outras colunas.

Por exemplo, a classe User pode incluir uma propriedade Address que representa uma composição de propriedades street, city, state e postCode. Para armazenar as colunas compostas separadamente na tabela, anote a propriedade Address na classe User com @Embedded. O snippet de código abaixo mostra essa configuração:

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

A tabela que representa um objeto User contém colunas com estes nomes: id, firstName, street, state, city e post_code.

Se uma entidade tiver várias propriedades incorporadas do mesmo tipo, é possível manter cada coluna exclusiva definindo a prefix propriedade. O Room adiciona o valor fornecido ao início do nome de cada coluna no objeto incorporado.