Cómo definir relaciones entre objetos

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 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 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 muestran de multimapa

En el método que muestra tipos de datos de multimapa, no necesitas definir ninguna clase de datos adicional. En su lugar, defines el tipo de datos que se muestran 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 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 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 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 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 corresponde a una sola 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, 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 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 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 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 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 objetos Song 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 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 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.

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. 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

Blogs