En el post anterior veíamos cómo se crean Entidades en Room, la API para persistencia en SQlite de Android. Hoy vamos a continuar con las Entidades pero hablando de las relaciones entre ellas, relaciones uno a uno, relaciones uno a muchos y muchos a muchos..

Antes de nada debemos conocer que son los objetos embebidos ya que gracias a estos podremos realizar las diferentes relaciones entre “tablas” en Room.

Capítulos Persistencia en Android con Room

Objetos embebidos o incorporados

Vamos a suponer que queremos modelar un objeto llamado User para persistir en base de datos. Para eso tendremos que crear una Entidad User tal que así:

@Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
)

Ahora queremos añadir a este usuario un nuevo atributo en el que vamos a definir la dirección postal del usuario. Primero creamos la data class que modela esa dirección postal:

data class Address(
        val street: String?,
        val state: String?,
        val city: String?,
        @ColumnInfo(name = "post_code") val postCode: Int
    )

Como vemos no anotamos la data class como Entity ya que estos campos se guardarán dentro de la “tabla” User. Ahora añadimos el campo a la entidad User y para ello hacemos:

@Entity
    data class User(
        @PrimaryKey val id: Int,
        val firstName: String?,
        @Embedded val address: Address?
    )

Con la anotación @Embedded estamos indicando a Room que ese campo es de tipo embebido o incrustado y que los parámetros que componen a ese objeto serán columnas también de la entidad en donde se declara.

La entidad User anterior lo que creará será una tabla con los campos o columnas tales que: id, firstName, street, state, city y post_code

 Un campo embebido puede a su vez incluir otro campo embebido.

Relaciones Uno a Uno en Room para Android

En este tipo de relaciones tenemos dos Entidades que tendrán una relación entre ellas de uno a uno. Una entidad tendrá una relación única con la otra entidad y la otra entidad tendrá asimismo una relación única con la entidad primera. Por ejemplo, vamos a establecer una relación entre usuarios y bibliotecas en donde cada usuario solo podrá tener relación con una biblioteca y cada biblioteca sólo podrá ser de un único usuario. Típica relación uno a uno.

Primero deberíamos de crear las dos entidades:

@Entity
    data class User(
        @PrimaryKey val userId: Long,
        val name: String,
        val age: Int
    )

y ahora la clase de biblioteca:

@Entity
    data class Library(
        @PrimaryKey val libraryId: Long,
        val userOwnerId: Long
    )

En esta segunda data class vemos que hay un atributo que hace referencia a la clave primaria de la otra entidad, userOwnerId, es obligatorio que una de las dos entidades posea esta propiedad. 

La entidad que no incluya la propiedad que apunta a la clave primaria de la otra entidad se denominará Entidad Principal y a la otra Entidad Secundaria.

Ahora para poder consultar el listado de usuarios relacionando cada uno con su biblioteca debemos de crear justamente una clase que modele esa relación.

data class UserAndLibrary(
        @Embedded val user: User,
        @Relation(
             parentColumn = "userId",
             entityColumn = "userOwnerId"
        )
        val library: Library
    )

En esta clase que modela la relación entre ambas entidades debemos de tener una propiedad anotada como objeto embebido que será del tipo de la Entidad Primaria. En nuestro caso de la clase User.

Debemos de añadir también una anotación @Relation a una propiedad que será de tipo Entidad Secundaria y donde indicaremos a través de que campos están relacionadas ambas entidades. En nuestro caso  userId de la Entidad Primaria y userOwnerId de la clase secundaria.

Por último hay que añadir en el DAO la query que realizará el “select” de la base de datos:

@Transaction
    @Query("SELECT * FROM User")
    fun getUsersAndLibraries(): List<UserAndLibrary>

El tipo de datos que devolverá esta consulta es del tipo que hemos modelado antes con el objeto embebido y la relación entre ambas entidades.

Además este método tendrá que estar anotado como @Transaction ya que internamente se realizan varias consultas y por ello todo debe de ejecutarse bajo una única transacción.

Relaciones Uno a Muchos en Room para Android

En este tipo de relaciones prácticamente todo es igual que en el caso anterior solo que en vez de existir una instancia de cada una de las entidades relacionadas entre sí, en esta ocasión existirá una lista de instancias de una entidad relacionadas con la otra única entidad.

Por ejemplo, vamos a ver como sería cuando un objeto Usuario posee una lista de Playlist de música. En este caso cada usuario tendrá muchas playlists asociadas.

Empezamos modelando las clases User y Playlist:

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

La entidad principal será de nuevo la clase User y la secundaria será la PlayList. De nuevo esta clase secundaria contiene una propiedad que apunta a la clave primaria de la entidad principal, userCreatorId.

 También debemos de modelar la clase donde definimos la relación como vemos a continuación.

data class UserWithPlaylists(
        @Embedded val user: User,
        @Relation(
              parentColumn = "userId",
              entityColumn = "userCreatorId"
        )
        val playlists: List<Playlist>
    )

Es igual que en la relación uno a uno, solo que el atributo que representa a la entidad secundaria será de tipo List<PlayList> ya que estamos en una relación uno a muchos.

El último paso es exactamente igual que antes, donde creábamos la consulta en el DAO pertinente:

@Transaction
    @Query("SELECT * FROM User")
    fun getUsersWithPlaylists(): List<UserWithPlaylists>

Relaciones Muchos a Muchos en Room para Android

En este tipo de relaciones entre dos entidades, cada instancia de la entidad principal corresponde a cero o más instancias de la entidad secundaria y viceversa. 

Para nuestro ejemplo vamos a suponer que tenemos canciones y listas de canciones, las PlayList del ejemplo anterior. Cada canción puede pertenecer a muchas PlayList y una PlayList puede tener muchas canciones.

Creamos las dos clases que modelan a las entidades:

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

Vemos que en este caso no hay Entidades principales o secundarias y las relaciones se modelan en una Entidad externa. En realidad es como crear una tabla con las relaciones, es como se hace en cualquier relación de muchos a muchos en una base de datos relacional típica.

@Entity(primaryKeys = ["playlistId", "songId"])
    data class PlaylistSongCrossRef(
        val playlistId: Long,
        val songId: Long
    )

En esta Entidad de relaciones, indicamos los atributos de las claves primarias de las dos entidades relacionadas.

A la hora de realizar la consulta debemos de tener en cuenta que esta consulta puede hacerse de dos formas diferentes:

  • Si queremos conocer cada PlayList y cada PlayList que integre su lista de canciones.
  • O si queremos consultar las canciones y saber en que PlayList está presente cada canción.

En el primer caso la clase que modela la respuesta será:

data class PlaylistWithSongs(
        @Embedded val playlist: Playlist,
        @Relation(
             parentColumn = "playlistId",
             entityColumn = "songId",
             associateBy = @Junction(PlaylistSongCrossRef::class)
        )
        val songs: List<Song>
    )

Esta clase devolverá una PlayList y relacionada con esta, un listado de canciones que pertenecen a esta PlayList.

Para el segundo caso la clase será:

data class SongWithPlaylists(
        @Embedded val song: Song,
        @Relation(
             parentColumn = "songId",
             entityColumn = "playlistId",
             associateBy = @Junction(PlaylistSongCrossRef::class)
        )
        val playlists: List<Playlist>
    )

En este caso obtendremos una canción y un listado de PlayList donde esa canción está presente.

Por último faltaría incluir en el DAO la query a ejecutar para la consulta propiamente que en nuestro caso serán dos consultas:

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

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