Définir des relations entre des objets

SQLite étant une base de données relationnelle, vous pouvez définir des relations entre les entités. Cependant, alors que la plupart des bibliothèques de mappage relationnel d'objets permettent aux entités de se référencer entre elles, Room interdit explicitement cette pratique. Pour en savoir plus sur le raisonnement technique qui justifie cette décision, consultez Comprendre pourquoi Room n'autorise pas les références d'objets.

Deux approches possibles

Dans Room, il existe deux façons de définir et d'interroger une relation entre les entités : en utilisant une classe de données intermédiaire avec des objets intégrés ou une méthode de requête relationnelle avec un type renvoyé en multimap.

Classe de données intermédiaire

Dans l'approche de la classe de données intermédiaire, vous définissez une classe de données qui représente la relation entre vos entités Room. Cette classe de données contient les associations entre les instances d'une entité et les instances d'une autre entité en tant qu'objets intégrés. Vos méthodes de requête peuvent ensuite renvoyer des instances de cette classe de données à utiliser dans votre application.

Par exemple, vous pouvez définir une classe de données UserBook pour représenter les utilisateurs de la bibliothèque avec des livres spécifiques récupérés, et définir une méthode de requête pour récupérer une liste d'instances UserBook à partir de la base de données :

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

Types renvoyés en multimap

Avec l'approche de type renvoyé en multimap, vous n'avez pas besoin de définir d'autres classes de données. Spécifiez plutôt un type renvoyé en multimap pour la méthode basée sur la structure de carte que vous souhaitez, ainsi qu'une relation entre vos entités, directement dans votre requête SQL.

Par exemple, la méthode de requête suivante renvoie un mapping de User et de Book pour représenter les utilisateurs de la bibliothèque avec des livres spécifiques récupérés :

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();

Choisir une approche

Room est compatible avec ces deux approches. Vous pouvez donc choisir celle qui convient le mieux à votre application. Cette section décrit certaines des raisons pour lesquelles vous pourriez préférer l'une ou l'autre.

L'approche basée sur des classes de données intermédiaires vous permet d'éviter d'écrire des requêtes SQL complexes, mais elle peut également augmenter la complexité du code en raison des classes de données supplémentaires dont elle a besoin. Pour résumer, l'approche liée à un type renvoyé en multimap requiert davantage d'opérations de vos requêtes SQL, tandis que l'approche de classe de données intermédiaire nécessite davantage de travail dans votre code.

Si vous n'avez aucune raison spécifique d'utiliser des classes de données intermédiaires, nous vous recommandons d'utiliser le type renvoyé en multimap. Pour en savoir plus sur cette approche, consultez Renvoi en multimap.

Le reste de ce guide se concentre sur la définition de relations au moyen de classes de données intermédiaires.

Créer des objets intégrés

Parfois, vous souhaitez représenter une entité ou un objet de données comme un ensemble cohérent dans votre logique de base de données, même si l'objet contient plusieurs champs. Dans ce genre de situations, vous pouvez utiliser l'annotation @Embedded pour représenter un objet que vous souhaitez décomposer en ses sous-champs dans un tableau. Vous pouvez ensuite interroger les champs intégrés comme vous le feriez pour d'autres colonnes individuelles.

Par exemple, votre classe User peut inclure un champ de type Address, qui représente une composition de champs nommés street, city, state et postCode. Pour stocker les colonnes composées séparément dans le tableau, incluez un champ Address dans la classe User, annotée avec @Embedded, comme l'illustre l'extrait de code suivant :

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

Le tableau représentant un objet User contient des colonnes portant les noms suivants : id, firstName, street, state, city et post_code.

Si une entité comporte plusieurs champs intégrés du même type, vous pouvez garder chaque colonne unique en définissant la propriété de prefix. Room ajoute ensuite la valeur définie en tête de chaque colonne dans l'objet intégré.

Définir des relations de type un à un

Une relation de type un à un entre deux entités est une relation dans laquelle chaque instance de l'entité parente correspond exactement à une instance de l'entité enfant, et inversement.

Prenons l'exemple d'une application de streaming musical où l'utilisateur possède une bibliothèque de titres dont il est propriétaire. Chaque utilisateur ne possède qu'une seule bibliothèque, qui correspond chacune à un seul utilisateur. Par conséquent, il existe une relation de type un à un entre l'entité User et l'entité Library.

Pour définir une relation de type un à un, créez d'abord une classe pour chacune de vos deux entités. L'une des entités doit inclure une variable qui fait référence à la clé primaire de l'autre entité.

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

Pour interroger la liste des utilisateurs et des bibliothèques correspondantes, vous devez d'abord représenter la relation de type un à un entre les deux entités. Pour ce faire, créez une classe de données dans laquelle chaque instance contient une instance de l'entité parente et l'instance correspondante de l'entité enfant. Ajoutez l'annotation @Relation à l'instance de l'entité enfant, avec parentColumn défini sur l'en-tête de la colonne de clé primaire de l'entité parente et entityColumn défini sur l'en-tête de la colonne de l'entité enfant qui référence la clé primaire de l'entité parente.

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

Enfin, ajoutez une méthode à la classe DAO qui renvoie toutes les instances de la classe de données qui associent l'entité parente et l'entité enfant. Cette méthode nécessite que Room exécute deux requêtes. Vous devez donc ajouter l'annotation @Transaction à cette méthode pour que l'ensemble de l'opération soit effectué de manière atomique.

Kotlin

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

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();

Définir des relations de type un à plusieurs

Une relation de type un à plusieurs entre deux entités est une relation dans laquelle chaque instance de l'entité parente correspond à aucune ou plusieurs instances de l'entité enfant, mais chaque instance de l'entité enfant ne peut correspondre qu'à une seule instance de l'entité parente.

Pour reprendre l'exemple de l'application de streaming musical, supposons que l'utilisateur ait la possibilité d'organiser ses titres dans des playlists. Chaque utilisateur peut créer autant de playlists qu'il le souhaite, mais chaque playlist ne peut être créée que par un seul utilisateur. Par conséquent, il existe une relation de un à plusieurs entre l'entité User et l'entité Playlist.

Pour définir une relation de type un à plusieurs, créez d'abord une classe pour les deux entités. Comme dans le cadre d'une relation de type un à un, l'entité enfant doit inclure une variable qui fait référence à la clé primaire de l'entité parente.

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

Pour interroger la liste des utilisateurs et des playlists correspondantes, vous devez d'abord représenter la relation de type un à plusieurs entre les deux entités. Pour ce faire, créez une classe de données dans laquelle chaque instance contient une instance de l'entité parente et une liste de toutes les instances d'entité enfant correspondantes. Ajoutez l'annotation @Relation à l'instance de l'entité enfant, avec parentColumn défini sur l'en-tête de la colonne de clé primaire de l'entité parente et entityColumn défini sur l'en-tête de la colonne de l'entité enfant qui référence la clé primaire de l'entité parente.

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

Enfin, ajoutez une méthode à la classe DAO qui renvoie toutes les instances de la classe de données qui associent l'entité parente et l'entité enfant. Cette méthode nécessite que Room exécute deux requêtes. Vous devez donc ajouter l'annotation @Transaction à cette méthode pour que l'ensemble de l'opération soit effectué de manière atomique.

Kotlin

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

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();

Définir des relations de type plusieurs à plusieurs

Une relation de type plusieurs à plusieurs entre deux entités est une relation dans laquelle chaque instance de l'entité parente correspond à zéro instance ou plus de l'entité enfant, et inversement.

Reprenons à nouveau l'exemple de l'application de streaming musical et penchons-nous sur les titres des playlists définies par l'utilisateur. Chaque playlist peut inclure de nombreux titres, et chacun d'eux peut faire partie de différentes playlists. Par conséquent, il existe une relation de plusieurs à plusieurs entre l'entité Playlist et l'entité Song.

Pour définir une relation de plusieurs à plusieurs, créez d'abord une classe pour chacune de vos deux entités. Les relations de type plusieurs à plusieurs se distinguent des autres types de relations, car il n'existe en général aucune référence à l'entité parente dans l'entité enfant. Créez plutôt une troisième classe pour représenter une entité associative (ou tableau de concordance) entre les deux entités. Le tableau de concordance doit comporter des colonnes pour la clé primaire de chaque entité dans la relation de type plusieurs à plusieurs représentée dans le tableau. Dans cet exemple, chaque ligne du tableau de concordance correspond à l'association d'une instance Playlist et d'une instance Song, où le titre référencé est inclus dans la playlist spécifiée.

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

L'étape suivante dépend de la façon dont vous souhaitez interroger ces entités associées.

  • Si vous souhaitez interroger des playlists et une liste des chansons correspondant à chaque playlist, créez une classe de données contenant un seul élément Playlist et une liste de tous les objets Song inclus dans la playlist.
  • Si vous souhaitez interroger des chansons et une liste des playlists correspondant à chaque chanson, créez une classe de données contenant un seul objet Song et une liste de tous les objets Playlist dans lesquels le titre est inclus.

Dans les deux cas, représentez la relation entre les entités à l'aide de la propriété associateBy de l'annotation @Relation dans chacune de ces classes pour identifier l'entité de concordance déterminant la relation entre l'entité Playlist et l'entité 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;
}

Enfin, ajoutez une méthode à la classe DAO pour afficher la fonction de requête nécessaire à votre application.

  • getPlaylistsWithSongs : cette méthode interroge la base de données et renvoie tous les objets PlaylistWithSongs obtenus.
  • getSongsWithPlaylists : cette méthode interroge la base de données et renvoie tous les objets SongWithPlaylists obtenus.

Ces méthodes nécessitent que Room exécute deux requêtes. Vous devez donc ajouter l'annotation @Transaction aux deux méthodes pour vous assurer que toute l'opération est effectuée de manière atomique.

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();

Définir des relations imbriquées

Vous pouvez parfois avoir besoin d'interroger un ensemble de trois tableaux ou plus, tous liés les uns aux autres. Dans ce cas, définissez des relations imbriquées entre les tableaux.

Supposons que, dans notre application de streaming musical, vous souhaitiez interroger tous les utilisateurs, toutes les playlists de chaque utilisateur et tous les titres de chaque playlist de chaque utilisateur. Les utilisateurs sont liés par une relation de type un à plusieurs aux playlists, qui sont elles-mêmes liées par une relation de type plusieurs à plusieurs avec les titres. L'exemple de code suivant montre les classes qui représentent ces entités, ainsi que le tableau de concordance pour la relation de type plusieurs à plusieurs entre les playlists et les titres :

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

Tout d'abord, représentez la relation entre deux des tableaux de votre ensemble comme vous le feriez normalement, avec une classe de données et l'annotation @Relation. L'exemple suivant montre une classe PlaylistWithSongs qui représente une relation de type plusieurs à plusieurs entre la classe d'entités Playlist et la classe d'entités 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;
}

Après avoir défini une classe de données représentant cette relation, créez une autre classe de données qui représente la relation entre un autre tableau de votre ensemble et la première classe de la relation, en "imbriquant" la relation existante dans la nouvelle. L'exemple suivant montre une classe UserWithPlaylistsAndSongs qui représente une relation de type un à plusieurs entre la classe d'entités User et la classe de relation 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 classe UserWithPlaylistsAndSongs représente de manière indirecte les relations entre les trois classes d'entités : User, Playlist et Song. C'est ce qu'illustre l'image 1.

UserWithPlaylistsAndSongs représente la relation entre l&#39;utilisateur et PlaylistWithSongs, qui à son tour représente la relation entre la playlist et le titre.

Figure 1 : Schéma de classes de relations dans l'exemple d'application de streaming musical.

Si votre ensemble compte d'autres tableaux, créez une classe permettant de représenter la relation entre chaque tableau restant et la classe de relation qui représente les relations entre tous les tableaux précédents. Cette opération crée une chaîne de relations imbriquées entre tous les tableaux que vous souhaitez interroger.

Enfin, ajoutez une méthode à la classe DAO pour afficher la fonction de requête nécessaire à votre application. Cette méthode nécessite que Room exécute plusieurs requêtes. Vous devez donc ajouter l'annotation @Transaction pour vous assurer que toute l'opération est effectuée de manière atomique :

Kotlin

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();

Ressources supplémentaires

Pour en savoir plus sur la définition de relations entre des entités dans Room, consultez les ressources supplémentaires ci-dessous.

Exemples

Vidéos

Blogs