Référencer des données complexes avec Room

Room peut convertir des types primitifs et encadrés, mais ne permet pas de faire référence à des objets entre des entités. Découvrez comment utiliser les convertisseurs de type et pourquoi Room ne permet pas les références d'objets.

Utiliser des convertisseurs de type

Votre application doit parfois stocker un type de données personnalisé dans une seule colonne de base de données. Pour utiliser des types personnalisés, vous pouvez fournir des convertisseurs de types. Il s'agit de fonctions qui indiquent à Room comment convertir des types personnalisés en types connus et des types connus en types personnalisés que Room peut conserver. Pour identifier les convertisseurs de types, utilisez l' @ColumnTypeConverter annotation.

Supposons que vous deviez conserver des instances de Date dans votre base de données Room. Room ne peut pas conserver nativement les objets Date. Vous devez donc définir des convertisseurs de types :

object Converters {
    @ColumnTypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @ColumnTypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

Cet exemple définit deux fonctions de convertisseur de type : une qui convertit un objet Date en objet Long, et une qui reconvertit un objet Long en objet Date. Étant donné que Room peut conserver les objets Long, il peut utiliser ces convertisseurs pour les objets Date.

Ensuite, ajoutez l'@ColumnTypeConverters annotation à la classe AppDatabase afin que Room puisse utiliser la classe de convertisseur que vous avez définie :

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Une fois ces convertisseurs définis, vous pouvez utiliser votre type personnalisé dans vos entités et vos DAO, de la même manière que les types primitifs :

@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val birthday: Date?
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE birthday = :targetDate")
    suspend fun findUsersBornOnDate(targetDate: Date): List<User>
}

Dans cet exemple, Room peut utiliser le convertisseur de type défini n'importe où, car vous avez annoté AppDatabase avec @ColumnTypeConverters. Pour annoter des entités ou des DAO spécifiques, ajoutez @ColumnTypeConverters à vos classes @Entity ou @Dao.

Contrôler l'initialisation du convertisseur de type

Généralement, Room instancie les convertisseurs de types pour vous. Toutefois, si vous devez transmettre des dépendances supplémentaires à vos classes de convertisseur de type, votre application doit contrôler directement leur initialisation. Dans ce cas, annotez votre classe de convertisseur avec @ProvidedColumnTypeConverter :

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

En plus de déclarer votre classe de convertisseur dans @ColumnTypeConverters, utilisez la RoomDatabase.Builder.addColumnTypeConverter fonction pour transmettre une instance de votre classe de convertisseur au compilateur RoomDatabase :

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

Pourquoi Room ne permet pas les références d'objets

Point clé à retenir : Room interdit de référencer des objets entre des classes d'entités. Vous devez donc demander explicitement les données dont votre application a besoin.

Le mappage des relations d'une base de données avec le modèle d'objet correspondant est une pratique courante qui fonctionne très bien côté serveur. Même lorsque le programme charge les propriétés au fur et à mesure de leur accès, le serveur reste performant.

Toutefois, côté client, ce type de chargement différé n'est pas possible, car il se produit généralement sur le thread UI et que l'interrogation des informations sur disque dans ce thread engendre des problèmes de performances significatifs. Le thread UI dispose généralement d'environ 16 ms pour calculer et dessiner la mise à jour d'une activité. Par conséquent, même si une requête ne prend que 5 ms, il est probable que votre application soit à court de temps pour générer le frame, ce qui provoquera des problèmes visuels. L'exécution de la requête peut prendre encore plus de temps si une transaction distincte a lieu en parallèle ou si l'appareil exécute d'autres tâches nécessitant une grande quantité de disque. Parallèlement, si vous n'utilisez pas le chargement différé, votre application récupère plus de données que nécessaire, ce qui crée des problèmes de consommation de mémoire.

Le choix du mappage revient habituellement aux développeurs qui peuvent ainsi déterminer l'approche la plus adaptée en fonction des cas d'utilisation de leur application. Ils décident généralement de partager le modèle entre leur application et l'interface utilisateur. Cependant, cette solution n'est pas très évolutive, car à mesure que l'UI change, le modèle partagé crée des problèmes difficiles à anticiper et à déboguer pour les développeurs.

Prenons l'exemple d'une interface utilisateur qui charge une liste d'objets Book, où chaque livre est associé à un objet Author. Vous pouvez commencer par concevoir vos requêtes pour utiliser le chargement différé afin que les instances de Book récupèrent l'auteur. La première récupération de la propriété author interroge la base de données. Quelque temps plus tard, vous vous rendez compte que vous devez également afficher le nom de l'auteur dans l'UI de votre application. Vous pouvez accéder à ce nom, comme illustré dans l'extrait de code suivant :

Text(text = book.author.name)

Toutefois, cette modification apparemment anodine entraîne l'interrogation de la table Author sur le thread principal.

Si vous interrogez les informations sur l'auteur à l'avance, mais que vous n'en avez pas besoin, il est difficile de modifier le mode de chargement des données. Par exemple, si l'UI de votre application n'a plus besoin d'afficher les informations Author, votre application charge quand même les données qu'elle n'affiche pas, ce qui gaspille de l'espace mémoire précieux. L'efficacité de votre application se dégrade encore plus si la classe Author fait référence à une autre table, telle que Books.

Pour référencer plusieurs entités en même temps à l'aide de Room, créez un objet de données contenant chaque entité, puis écrivez une requête qui joint les tables correspondantes. Ce modèle bien structuré, associé aux puissantes fonctionnalités de validation des requêtes de Room, permet à votre application de consommer moins de ressources lors du chargement des données, améliorant ainsi les performances de votre application et l'expérience utilisateur.