Cómo definir relaciones entre objetos

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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 modelar la relación mediante una clase de datos intermedia con objetos incorporados o un método de búsquedas relacionadas con un tipo de datos que se muestra 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 mostrar instancias de esta clase de datos para usar en tu app.

Por ejemplo, puedes definir una clase de datos UserBook a fin de representar a los usuarios de la biblioteca que retiraron determinados libros y definir un método de búsqueda bpara 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 muestran de multimapa

En el método de tipos de datos que se muestran de multimapa, no necesitas definir ninguna clase de datos adicional. En su lugar, debes definir un tipo de datos que se muestra de multimapa para tu método según la estructura de mapa que desees y definir la relación entre tus entidades directamente en tu consulta en SQL.

Por ejemplo, el siguiente método de búsqueda muestra 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 los dos enfoques descritos anteriormente, por lo que debes 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 de tipo de datos que se muestra 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 de tipo de datos que se muestra de multimapa. Para obtener más información sobre este enfoque, consulta Cómo mostrar 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 se corresponde exactamente con una instancia de la entidad secundaria y viceversa.

Por ejemplo, imagina una app de streaming 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, debe haber una relación de uno a uno entre la entidad User y la entidad Library.

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;
}

A fin de 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 muestre 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 streaming de música, supongamos que el usuario puede organizar las canciones en listas de reproducción. Cada usuario puede crear tantas listas de reproducción como desee, pero un solo usuario crea cada lista de reproducción. Por lo tanto, debería haber una relación de uno a varios entre la entidad User y la entidad Playlist.

Primero, crea una clase para cada una de las dos entidades. Como en el ejemplo anterior, 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;
}

A fin de buscar la lista de usuarios y las listas de reproducción 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 muestre 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 streaming de música, imagina nuevamente las listas de reproducción definidas por el usuario. Cada lista de reproducción puede incluir muchas canciones y cada canción puede ser parte de muchas listas diferentes. Por lo tanto, debería haber una relación de varios a varios entre la entidad Playlist y la entidad Song.

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 lista de reproducción 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 buscar listas de reproducción y un listado de las canciones correspondientes por cada lista de reproducción, crea una clase de datos nueva con un objeto Playlist único y un listado de todos los objetos Song que incluye la lista de reproducción.
  • Si quieres buscar canciones y un listado de las listas de reproducción correspondientes por cada canción, crea una clase de datos nueva con un objeto Song único y un listado de los objetos Playlist 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 muestra todos los objetos PlaylistWithSongs resultantes.
  • getSongsWithPlaylists: Este método busca la base de datos y muestra todos los objetos SongWithPlaylists 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 streaming de música, quieres buscar todos los usuarios, todas las listas de reproducción de cada uno de ellos y todas las canciones de cada lista de cada usuario. Los usuarios tienen una relación uno a varios con las listas de reproducción, 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 listas de reproducción 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.

UserWithPlaylistsAndSongs modela la relación entre User y PlaylistWithSongs, que a su vez modela la relación entre Playlist y Song.

Figura 1: Diagrama de clases de relaciones en el ejemplo de la app de streaming de música

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. Así se crea una cadena de relaciones anidadas entre todas las tablas que quieres buscar.

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

Blogs