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

Room permet de convertir des types primitifs en types encadrés (et inversement), mais ne permet pas de faire référence à des objets entre des entités. Ce document explique comment utiliser les convertisseurs de type et décrit 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, 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'annotation @TypeConverter.

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

Kotlin

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

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

Java

public class Converters {
  @TypeConverter
  public static Date fromTimestamp(Long value) {
    return value == null ? null : new Date(value);
  }

  @TypeConverter
  public static Long dateToTimestamp(Date date) {
    return date == null ? null : date.getTime();
  }
}

Cet exemple définit deux méthodes de convertisseur de type : l'une qui convertit un objet Date en objet Long et l'autre qui effectue la conversion inverse (conversion de Long en Date). Étant donné que Room sait comment conserver les objets Long, il peut utiliser ces convertisseurs pour les objets Date.

Vous allez maintenant ajouter l'annotation @TypeConverters à la classe AppDatabase pour que Room connaisse la classe de conversion que vous avez définie :

Kotlin

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

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract 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 :

Kotlin

@Entity
data class User(private val birthday: Date?)

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

Java

@Entity
public class User {
  private Date birthday;
}

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

Dans cet exemple, Room peut utiliser le convertisseur de type défini n'importe où, car vous avez annoté AppDatabase avec @TypeConverters. Toutefois, vous pouvez également annoter des entités ou des DAO spécifiques en ajoutant à vos classes @Entity ou @Dao l'annotation @TypeConverters.

Contrôler l'initialisation du convertisseur de type

Généralement, Room gère l'instanciation des convertisseurs de types pour vous. Cependant, il peut être nécessaire de transmettre des dépendances supplémentaires à vos classes de convertisseur de type, ce qui signifie que votre application doit contrôler directement l'initialisation des convertisseurs. Dans ce cas, annotez la classe de vos convertisseurs avec @ProvidedTypeConverter :

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

Ensuite, en plus de déclarer la classe de vos convertisseurs dans @TypeConverters, utilisez la méthode RoomDatabase.Builder.addTypeConverter() pour transmettre une instance de cette classe à RoomDatabase :

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(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 champs au fur et à mesure que l'utilisateur y accède, le serveur continue à fonctionner correctement.

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 du champ author interroge la base de données. Quelque temps plus tard, vous réalisez que vous devez également afficher le nom de l'auteur dans l'interface utilisateur de votre application. Vous pouvez accéder à ce nom facilement, comme illustré dans l'extrait de code suivant :

Kotlin

authorNameTextView.text = book.author.name

Java

authorNameTextView.setText(book.getAuthor().getName());

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, il est difficile de modifier le mode de chargement des données par la suite si vous n'avez plus besoin de ces 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 plus, 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, vous devez créer un POJO contenant chaque entité, puis écrire 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.