Como SQLite es una base de datos relacional, puedes definir relaciones entre entidades. Aunque la mayoría de las bibliotecas de asignación relacional de objetos permiten que los objetos de entidad se hagan referencia entre sí, Room lo prohíbe explícitamente. Para obtener más información sobre el razonamiento técnico que respalda esta decisión, consulta Por qué Room no permite referencias a objetos.
Dos posibles enfoques
En Room, hay dos formas de definir y realizar búsquedas de una relación entre entidades: puedes usar una clase de datos intermedia con objetos incorporados o un método de búsquedas relacionadas que muestre un tipo de datos que se devuelve de multimapa .
Clase de datos intermedia
En el enfoque de clase de datos intermedio, puedes definir una clase de datos que modela la relación entre tus entidades de Room. Esta clase de datos contiene las vinculaciones entre instancias de una entidad e instancias de otra entidad como objetos incorporados. Los métodos de búsqueda pueden devolver instancias de esta clase de datos para usar en tu app.
Por ejemplo, puedes definir una clase de datos UserBook
para representar a los usuarios de la biblioteca que retiraron determinados libros y definir un método de búsqueda para recuperar una lista de instancias UserBook
de la base de datos:
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 datos que se devuelven de multimapa
En el método que muestra tipos de datos que se devuelven de multimapa, no necesitas definir ninguna clase de datos adicional. En su lugar, defines el tipo de datos que se devuelven de multimapa para tu método según la estructura de mapa que deseas y que define la relación entre tus entidades directamente en tu consulta en SQL.
Por ejemplo, el siguiente método de búsqueda devuelve una asignación de las instancias User
y Book
para representar a los usuarios de la biblioteca que retiraron determinados libros:
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();
Cómo elegir un enfoque
Room admite ambos enfoques, por lo que puedes usar el que funcione mejor para tu app. En esta sección, se analizan algunos de los motivos por los que podrías preferir uno de ellos.
El enfoque de clase de datos intermedio te permite evitar escribir consultas en SQL complejas, pero también puede dar como resultado una mayor complejidad del código debido a las clases de datos adicionales que requiere. En resumen, el enfoque que muestra tipos de datos que se devuelven de multimapa requiere que tus consultas en SQL hagan más trabajo; y el enfoque de clase de datos intermedio requiere que el código realice más trabajo.
Si no tienes un motivo específico para usar clases de datos intermedios, te recomendamos que uses el enfoque que muestra tipos de datos que se devuelven de multimapa. Para obtener más información sobre este enfoque, consulta Cómo devolver un multimapa.
En el resto de esta guía, se muestra la definición de relaciones mediante el enfoque intermedio de clase de datos.
Cómo crear objetos incorporados
Es posible que, a veces, quieras expresar una entidad o un objeto de datos como un solo elemento integral en la lógica de la base de datos, incluso si el objeto contiene varios campos. En esas situaciones, puedes usar la anotación @Embedded
para representar un objeto cuyos subcampos quieras desglosar en una tabla. Luego, puedes buscar los campos integrados tal como lo harías con otras columnas individuales.
Por ejemplo, la clase User
puede incluir un campo de tipo Address
, que representa una composición de campos llamados street
, city
, state
y postCode
. Para almacenar las columnas compuestas por separado en la tabla, incluye un campo Address
en la clase User
con anotaciones @Embedded
, como se muestra en el siguiente fragmento de código:
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; }
La tabla que representa un objeto User
contiene columnas con los siguientes nombres: id
, firstName
, street
, state
, city
y post_code
.
Si una entidad tiene varios campos incorporados del mismo tipo, puedes establecer cada columna como única mediante la configuración de la propiedad prefix
. Luego, Room agrega el valor proporcionado al comienzo de cada nombre de columna en el objeto incorporado.
Cómo definir relaciones de uno a uno
En las relaciones de uno a uno entre dos entidades, cada instancia de la entidad principal corresponde a una sola instancia de la entidad secundaria y viceversa.
Por ejemplo, imagina una app de transmisión de música en la que el usuario tiene una biblioteca de canciones de su propiedad. Cada usuario tiene una sola biblioteca y cada biblioteca corresponde exactamente a un usuario. Por lo tanto, existe una relación de uno a uno entre la entidad User
y la entidad Library
.
Para definir una relación de uno a uno, primero crea una clase para cada una de las dos entidades. Una de las entidades debe incluir una variable que haga referencia a la clave primaria de la otra entidad.
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 la lista de usuarios y las bibliotecas correspondientes, primero debes modelar la relación de uno a uno entre las dos entidades. Para ello, crea una clase de datos nueva en la que cada instancia tenga una instancia de la entidad principal y la instancia correspondiente de la entidad secundaria. Agrega la anotación @Relation
a la instancia de la entidad secundaria, y asigna a parentColumn
el nombre de la columna de clave primaria de la entidad principal y a entityColumn
el nombre de la columna de la entidad secundaria que hace referencia a la clave primaria de la entidad principal.
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 último, agrega un método a la clase DAO que devuelve todas las instancias de la clase de datos que vincula la entidad principal con la secundaria. Este método requiere que Room ejecute dos búsquedas, así que agrega la anotación @Transaction
al método para asegurarte de que toda la operación se realice automáticamente.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary>
Java
@Transaction @Query("SELECT * FROM User") public List<UserAndLibrary> getUsersAndLibraries();
Cómo definir relaciones de uno a varios
En las relaciones de uno a varios entre dos entidades, cada instancia de la entidad principal corresponde a cero o más instancias de la entidad secundaria, pero cada instancia de la entidad secundaria solo puede corresponder una instancia de la entidad principal.
En el ejemplo de la app de transmisión de música, supongamos que el usuario puede organizar las canciones en playlists. Cada usuario puede crear tantas playlists como desee, pero un solo usuario crea cada playlist. Por lo tanto, existe una relación de uno a varios entre la entidad User
y la entidad Playlist
.
Para definir una relación de uno a varios, primero crea una clase para las dos entidades. Al igual que en una relación de uno a uno, la entidad secundaria debe incluir una variable que haga referencia a la clave primaria de la entidad principal.
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 la lista de usuarios y las playlists correspondientes, primero debes modelar la relación de uno a varios entre las dos entidades. Para ello, crea una clase de datos nueva en la que cada instancia tenga una instancia de la entidad principal y una lista de todas las instancias de entidades secundarias correspondientes. Agrega la anotación @Relation
a la instancia de la entidad secundaria, y asigna a parentColumn
el nombre de la columna de clave primaria de la entidad principal y a entityColumn
el nombre de la columna de la entidad secundaria que hace referencia a la clave primaria de la entidad principal.
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 último, agrega un método a la clase DAO que devuelve todas las instancias de la clase de datos que vincula la entidad principal con la secundaria. Este método requiere que Room ejecute dos búsquedas, así que agrega la anotación @Transaction
al método para asegurarte de que toda la operación se realice automáticamente.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylists> getUsersWithPlaylists();
Cómo definir relaciones de varios a varios
En las relaciones de varios a varios entre dos entidades, cada instancia de la entidad principal corresponde a cero o más instancias de la entidad secundaria y viceversa.
En el ejemplo de la app de transmisión de música, imagina las canciones en las playlists definidas por el usuario.
Cada playlist puede incluir muchas canciones y cada canción puede ser parte de muchas playlists diferentes. Por lo tanto, existe una relación de varios a varios entre la entidad Playlist
y la entidad Song
.
Para definir una relación de varios a varios, primero crea una clase para cada una de las dos entidades. Las relaciones de varios a varios son diferentes de otros tipos de relación porque, por lo general, no hay una referencia a la entidad principal en la entidad secundaria. Entonces, crea una tercera clase para representar una entidad asociativa (o tabla de referencias cruzadas) entre las dos entidades. La tabla de referencias cruzadas debe tener columnas para la clave primaria de cada entidad contemplada en la relación de varios a varios que se representa en la tabla. En este ejemplo, cada fila de la tabla de referencias cruzadas corresponde a una vinculación de una instancia Playlist
y una instancia Song
donde la canción a la que se hace referencia se incluye en la playlist a la que se hace referencia.
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; }
El siguiente paso depende de cómo quieras consultar las entidades relacionadas.
- Si quieres consultar playlists y un listado de las canciones correspondientes por cada playlist, crea una clase de datos nueva con un objeto
Playlist
único y un listado de todos los objetosSong
que incluye la playlist. - Si quieres consultar canciones y un listado de las playlists correspondientes por cada canción, crea una clase de datos nueva con un objeto
Song
único y un listado de los objetosPlaylist
en los que se incluye la canción.
En ambos casos, modela la relación entre las entidades mediante la propiedad associateBy
en la anotación @Relation
de cada una de las clases para identificar la entidad de la referencia cruzada que proporciona la relación entre las entidades Playlist
y 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 último, agrega un método a la clase DAO para exponer la funcionalidad de búsqueda que necesita la app.
getPlaylistsWithSongs
: Este método busca la base de datos y devuelve todos los objetosPlaylistWithSongs
resultantes.getSongsWithPlaylists
: Este método busca la base de datos y devuelve todos los objetosSongWithPlaylists
resultantes.
Cada uno de los métodos requiere que Room ejecute dos búsquedas, así que agrega la anotación @Transaction
a ambos para asegurarte de que toda la operación se realice automáticamente.
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();
Cómo definir relaciones anidadas
Es posible que, en ocasiones, debas buscar un conjunto de tres o más tablas relacionadas entre sí. En ese caso, definirías relaciones anidadas entre las tablas.
Supongamos que, en el ejemplo de la app de transmisión de música, quieres consultar todos los usuarios, todas las playlists de cada uno de ellos y todas las canciones de cada playlist de cada usuario. Los usuarios tienen una relación uno a varios con las playlists, y estas tienen una relación varios a varios con las canciones. En el siguiente ejemplo de código, se muestran las clases que representan las entidades, así como la tabla de referencias cruzadas de la relación de varios a varios entre las playlists y las canciones:
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; }
Primero, modela la relación entre dos de las tablas del conjunto como lo harías normalmente, con una clase de datos y la anotación @Relation
. En el siguiente ejemplo, se muestra una clase PlaylistWithSongs
que modela una relación de varios a varios entre las clases de entidad Playlist
y 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; }
Después de definir una clase de datos que represente esa relación, crea otra clase de datos que modele la relación entre otra tabla del conjunto y la primera clase de relación, y "anida" la relación existente dentro de la nueva. En el siguiente ejemplo, se muestra una clase UserWithPlaylistsAndSongs
que modela una relación de uno a varios entre la clase de entidad User
y la clase de relación 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; }
La clase UserWithPlaylistsAndSongs
modela indirectamente las relaciones entre las tres clases de entidad User
, Playlist
y Song
. Esto se ilustra en la figura 1.
Si hay más tablas en el conjunto, crea una clase para modelar la relación entre cada tabla restante y la clase de relación que modela las relaciones entre todas las tablas anteriores. Esto crea una cadena de relaciones anidadas entre todas las tablas que quieres consultar.
Por último, agrega un método a la clase DAO para exponer la funcionalidad de búsqueda que necesita la app. El método requiere que Room ejecute varias búsquedas, así que agrega la anotación @Transaction
para asegurarte de que toda la operación se realice automáticamente:
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
Recursos adicionales
Para obtener más información sobre cómo definir relaciones entre entidades en Room, consulta los siguientes recursos adicionales.
Ejemplos
Videos
- What's New in Room (Android Dev Summit 2019)