• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/09/2024

Créez vos entités et convertisseurs de type

Pour que l’ORM puisse mapper (ou associer) le modèle objet avec une base de données SQLite, il faut maintenant annoter ces classes avec les annotations proposées par Room.

Annotez vos classes

L’annotation@Entitypour définir une table

La première annotation que nous allons utiliser est l’annotation @Entity. Il s’agit d’un élément essentiel de la bibliothèque Room qui permet de définir des classes comme des entités. Une entité correspond en réalité à une table dans la base de données. Chaque instance de cette entité correspond alors à une ligne dans la table correspondante en base de données.

Cette annotation est portée directement par la classe que l’on souhaite définir en tant qu’entité. Dans le cadre de la fonctionnalité que nous développons pour l’application PETiSoin, deux entités sont à créer :Animal etNote.

Si vous utilisez Java :                                      

Si vous utilisez Kotlin :                         

@Entity
public final class Animal
{
  public final int id;
  public final String type;
  public final String name;
  public final int height;
  public final int weight;
  public final int age;
  public final Address address;
  public Animal(int id, String type, 
  String name, int height, 
  int weight, int age, 
  Address address)
  {
    this.id = id;
    this.type = type;
    this.name = name;
    this.height = height;
    this.weight = weight;
    this.age = age;
    this.address = address;
  }
}
@Entity
public final class Note
{
  public final int id;
  public final String title;
  public final String description;
  public final long creationDate;
  public final int animalId;
  public Note(int id, 
  String title, String description, 
  long creationDate, 
  int animalId)
  {
    this.id = id;
    this.title = title;
    this.description = description;
    this.creationDate = creationDate;
    this.animalId = animalId;
  }
}
@Entity
data class Animal(
  val id: Int = 0,
  val type: String = "",
  val name: String? = null,
  val height: Int = 0,
  val weight: Int = 0,
  val age: Int = 0,
  val address: Address = Address()
)
@Entity
data class Note(
  val id: Int = 0,
  val title: String = "",
  val content: String = "",
  val creationDate: Long = 0L,
  val animalId: Int = 0
)

Contrairement aux autres classes, vous remarquerez que la classeAddress n’est pas encore annotée. C’est volontaire. L’idée n’est pas de stocker les informations de l’adresse dans une table spécifique de la base de données mais uniquement d’avoir une approche objet dans la description de notre modèle de données. Nous reviendrons sur la classeAddressun peu plus loin dans ce chapitre.

Est-ce qu’il est possible de donner un nom à une entité ? 

Par défaut, la table porte le nom de la classe sur laquelle l’annotation@Entity est présente. Si vous souhaitez personnaliser le nom de la table, utilisez l’attributtableName. Vous pouvez par exemple utiliser cet attribut pour que la tableNotes’appellenote_table

Si vous utilisez Java :

Si vous utilisez Kotlin :                      

@Entity(tableName = "note_table")
public final class Note
{
  public final int id;
  public final String title;
  public final String description;
  public final long creationDate;
  public final int animalId;
  public Note(int id, String title, 
  String description, long creationDate, 
  int animalId)
  {
    this.id = id;
    this.title = title;
    this.description = description;
    this.creationDate = creationDate;
    this.animalId = animalId;
  }
}
@Entity(tableName = "note_table")
data class Note(
  val id: Int = 0,
  val title: String = "",
  val content: String = "",
  val creationDate: Long = 0L,
  val animalId: Int = 0
)

L’annotation @PrimaryKey pour spécifier la clé primaire de l’entité

Maintenant que les entités sont définies, descendons d’un niveau pour s’intéresser à leurs attributs. Pour chacune de nos entités, nous allons commencer à nous intéresser aux clés primaires, soit les attributs  id des classes AnimaletNote.

Pour indiquer qu’un attribut est la clé primaire de la table dans votre base de données, il convient d’annoter l’attribut concerné avec@PrimaryKey.

Si vous utilisez Java :

Si vous utilisez Kotlin :

@Entity
public final class Animal
{
  @PrimaryKey
  public final int id;
  //...
}
@Entity
public final class Note
{
  @PrimaryKey
  public final int id;
  //...
}
@Entity
data class Animal(
  @PrimaryKey
  val id: Int = 0,
  //…
)
@Entity
data class Note(
  @PrimaryKey
  val id: Int = 0,
  //…
)

Si vous enregistrez cinq animaux dans la base de données sans jamais renseigner leur identifiant, ils porteront automatiquement les identifiants `1`, `2`, `3`, `4` et  `5`.

Bonne nouvelle, Room supporte l’auto-incrémentation ! Pour activer cette fonctionnalité :

  1. Utilisez l’attributautoGeneratede l’annotation@PrimaryKey.

  2. Donnez lui la valeurtrue. À noter que la fonctionnalité est désactivée par défaut.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@Entity
public final class Animal
{
  @PrimaryKey(autoGenerate = true)
  public final int id;
  //...
}

@Entity
public final class Note
{
  @PrimaryKey(autoGenerate = true)
  public final int id;
  //...
}
@Entity
data class Animal(
  @PrimaryKey(autoGenerate = true)
  val id: Int = 0,
  //…
)
@Entity
data class Note(
  @PrimaryKey(autoGenerate = true)
  val id: Int = 0,
  //…
)

L’annotation@ColumnInfopour personnaliser un champ

À l’image d’une entité, qui par défaut donne son nom à la table dans la base de données, le nom des différents champs d’une table est, par défaut, associé aux attributs de la classe. Si vous souhaitez personnaliser le nom du champ, utilisez l’annotation@ColumnInfoet son attributname. Cette annotation est tout particulièrement utile si vous activez l’obfuscation sur votre projet puisqu'elle permet alors de garder la main sur les noms des attributs dans la base de données.

Vous pouvez par exemple utiliser cette annotation et cet attribut sur la classeNote.

Si vous utilisez Java : 

Si vous utilisez Kotlin :                       

@Entity(tableName = "notes_table")
public final class Note
{
  @ColumnInfo(name = "noteId")
  @PrimaryKey(autoGenerate = true)
  public final int id;
  public final String title;
  @ColumnInfo(name = "content")
  public final String description;
  @ColumnInfo(name = "createdAt")
  public final long creationDate;
  public final int animalId;
  public Note(int id, String title, 
  String description, long creationDate, 
  int animalId)
  {
    this.id = id;
    this.title = title;
    this.description = description;
    this.creationDate = creationDate;
    this.animalId = animalId;
  }
}
@Entity(tableName = "note_table")
data class Note(
  @ColumnInfo(name = "noteId")
  @PrimaryKey(autoGenerate = true)
  val id: Int = 0,
  val title: String = "",
  @ColumnInfo(name = "content")
  val content: String = "",
  @ColumnInfo(name = "createdAt")
  val creationDate: Long = 0L,
  val animalId: Int = 0
)

L’annotation @Embeddedpour lier les classes entre elles

Nous avons ensemble défini les entités et personnalisé l’usage de certains attributs. L’implémentation proposée souffre cependant d’un grand manque : l’absence de lien entre les différents objets.

En effet, à ce stade, pour Room, aucun lien n’est réellement fait entre les classesAnimal,Address  etNote. Pour y remédier, commençons par créer du lien entre les classesAnimaletAddress. Pour l’implémentation du modèle objet de l’application PETiSoin, deux classes ont été créées pour porter les informations d’une adresse postale et d’un animal. Mais si l’on se réfère à notre modèle physique de données, tous les éléments qui composent l’adresse postale doivent se trouver dans la tableAnimal.

Capture d’écran qui montre l’intégralité des tables complétées Animal et Notes. Une flèche montre le lien entre les deux tables indiquant une relation 1:n.
L’adresse postale est associée à la table `Animal`
  1. Pour faire ce lien, utilisez alors l’annotation@Embeddedsur l’attributaddressde la classe  Animal. Cela permet de faire comprendre à Room que l’ensemble des attributs portés par la classeAddresssont bien à stocker dans la tableAnimal.

  2. L'annotation expose un attributprefixqui permet d’ajouter automatiquement un préfixe aux champs de classe encapsulés dans la table de la base de données. Par exemple, vous pourriez indiquer le mot cléaddress_en tant que valeur de l’attributprefixde l’annotation  @Embedded

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@Entity
public final class Animal
{
  //...
  @Embedded(prefix = "address_")
  public final Address address;
  //...
}
@Entity
data class Animal(
  //…
  @Embedded(prefix = "address_")
  val address: Address = Address()
)

Retour sur l’annotation@Entitypour lier les classes entre elles

Si les classesAnimaletNotemappent les deux tables de base de données, aucun lien n’est actuellement fait entre elles. Or, lors de la création du MDP, une relation de type 1:n avait été mise en place. Pour créer ce lien et modéliser cette relation , Room doit savoir que l’attributanimalIdde la classeNoteest en réalité une clé étrangère qui fait référence à l’attributid de la classeAnimal.

Nous allons revenir sur l’annotation@Entity. Bien que précédemment uniquement l’attributtableNameait été utilisé, cette annotation expose bien d’autres attributs dont un attributforeignKeysqui permet effectivement de renseigner les clés étrangères d’une entité.

Cet attribut accepte comme valeur un tableau deForeignKeys. Il s’agit également d’une annotation de la bibliothèque Room pour laquelle il est nécessaire de renseigner plusieurs attributs dont :

  • entityqui permet de déterminer à quelle entité la clé étrangère fait référence ;

  • parentColumnsqui permet de déterminer la clé primaire à laquelle la clé étrangère fait référence ;

  • childColumnsqui permet de déterminer la clé étrangère dans l’entité courante.

Dans le cadre de l’application PETiSoin, les données à renseigner sont donc les suivantes.

entity

parentColumns

childColumns

Animal

id

animalId

Nous pouvons alors passer à l’implémentation.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@Entity(
    tableName = "notes_table",
    foreignKeys = {
        @ForeignKey(
            entity = Animal.class,
            parentColumns = "id",
            childColumns = "animalId",
        )
    }
)
public final class Note
{
  //...
  public final int animalId;
  //...
}
@Entity(
  tableName = "note_table",
  foreignKeys = [ 
    ForeignKey(
      entity = Animal::class,
      parentColumns = ["id"],
      childColumns = ["animalId"]
    )
  ]
)
data class Note(
  //…
  val animalId: Int = 0
)

Voici une vidéo qui récapitule les principales étapes pour annoter les classes.

Créez un convertisseur de type

SQLite permet de mettre en place une base de données relationnelle. Et c’est exactement ce que nous venons de faire dans Room en définissant des relations entre les entités et donc les tables de la base de données.

Cependant SQLite présente des limitations, notamment sur les types de données qu’il prend en charge. Sur Android, SQLite supporte les types de données suivants :

  • TEXTqui permet de stocker des chaînes de caractères ;

  • INTEGERqui permet de stocker des nombres entiers ;

  • REALqui permet de stocker des nombres décimaux ;

  • BLOBqui permet de stocker des données binaires brutes comme des images, des fichiers audios, etc.

Heureusement pour nous, Room automatise le travail de conversion entre les types de données (primaire pour Java et basiques pour Kotlin) et les types associés en SQLite. Aussi, l’ensemble des types suivants qui ne sont pas inclus dans SQLite sont en réalité stockés en tant queINTEGERdans la base de données. Voici une liste de types pour manipuler les nombres entiers.

Si vous utilisez Java :

Si vous utilisez Kotlin : 

  • long

  • int

  • short

  • byte

  • Long

  • Int

  • Short

  • Byte

En plus de ces simples conversions, Room est très intelligent ! Avec un petit coup de pouce, il peut gérer des types plus complexes qui ne sont pas supportés par SQLite.

Pour illustrer cette intelligence, mettons en évidence le manque de contrôle évident dont l’implémentation de l’application PETiSoin actuelle souffre.  Comme vous le savez, l’application doit pouvoir stocker uniquement trois types (ou valeurs) d’animaux : des chats, des chiens et des lapins.

Dans l’implémentation actuelle, la nature de l’animal (chats, chiens, lapins) est porté par l’attributtypede la classeAnimalqui est une chaîne de caractères. Le fait que cet attribut soit une chaîne de caractères ne permet pas de contrôler les  valeurs qu’on lui donne. On pourrait accidentellement y mettre des valeurs incorrectes comme "éléphant" ou "cah"au lieu de "chat".  Pour s’assurer que seuls nos trois types d’animaux souhaités soient utilisés, il est possible d’introduire une énumération. Elle permet de définir un ensemble limité et précis de valeurs acceptées. Ici l’énumération pour définir les types d’animaux s’appelleAnimalType.

Si vous utilisez Java :

Si vous utilisez Kotlin : 

public enum AnimalType
{
    DOG,
    CAT,
    RABBIT
}
enum class AnimalType {
  DOG,
  CAT,
  RABBIT
}

Il est alors possible de faire évoluer le code de la classeAnimal.

Si vous utilisez Java :

Si vous utilisez Kotlin :                                           

@Entity
public final class Animal
{
  //...
  public final AnimalType type;
  //...
  public Animal(int id, AnimalType type, 
  String name, int height, 
  int weight, int age, 
  Address address)
  {
    this.id = id;
    this.type = type;
    this.name = name;
    this.height = height;
    this.weight = weight;
    this.age = age;
    this.address = address;
  }
}
@Entity
data class Animal(
  //…
  val type: AnimalType = AnimalType.DOG,
  //…
)

 Ok, nous avons défini l’énumération, mais comment la stocker maintenant dans notre base de données ? 

Bien que le type AnimalTypene soit pas un type de données supporté par SQLite, Room utilise en interne ce que l’on appelle un convertisseur de type afin de stocker la valeur de l’énumération sous forme de chaîne de caractères, ce qui correspond au typeTEXTdans la base de données.

Prenons un autre exemple. Dans l’implémentation actuelle des notes, la date de création est proposée sous la forme d’un timestamp ce qui n’est pas toujours évident à manipuler. Les langages de programmation Java et Kotlin proposent une classeDatequi semble tout à fait adaptée au besoin. Malheureusement, ni Room, ni SQLite ne savent manipuler la classeDate .

Vous allez donc devoir aider Room en créant votre propre convertisseur de type. Votre objectif est de convertir la date en timestamp.

1. Modifiez le code de la classeNotepour changer le type de l’attributcreationDate.

Si vous utilisez Java : 

Si vous utilisez Kotlin :                            

@Entity(/*...*/)
public final class Note
{
  //...
  public final Date creationDate;
  //...
  public Note(int id, String title, 
  String description, 
  Date creationDate, 
  int animalId)
  {
    this.id = id;
    this.title = title;
    this.description = description;
    this.creationDate = creationDate;
    this.animalId = animalId;
  }
}
@Entity(/*...*/)
data class Note(
  //…
  val creationDate: Date = Date(),
  //…
)

2. Créez une classe pour définir les convertisseurs de type dont nous avons besoin.  En Java elle contiendra deux méthodes statiques.

  • Une méthode pour convertir la date vers le timestamp.

  • Une méthode pour convertir le timestamp vers la date.

Ici, la classe se nomme sobrement "Converters".

Si vous utilisez Java :                                                                                                                                   

Si vous utilisez Kotlin, la logique est la même si ce n’est que les méthodes n’ont pas besoin d’être statiques :

public class Converters 
{
  public static Date fromTimestamp(Long value) 
  {
    return value == null ? null : new Date(value);
  }
  public static Long dateToTimestamp(Date date) 
  {
    return date == null ? null : date.getTime();
  }
}
class Converters
{
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

3. Annotez cette classe avec@TypeConverter pour expliciter les méthodes en tant que convertisseur de type.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

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();
  }
}
class Converters
{
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }
  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

Voici une vidéo qui récapitule les principales étapes pour créer un convertisseur de types.

À vous de jouer !

Contexte

Dans le chapitre précédent, vous avez créé le modèle objet utile à votre projet. Dans le projet, disponible sur GitHub (Java ou Kotlin), vous devez annoter votre modèle objet pour le transformer en entities.

Consignes

  1. Dans le package com.openclassRooms.Room.data.entity, vous devez annoter les classes  AnimaletVaccinepour les transformer en entités.

  2. Pour gérer correctement la date, mettez en place un convertisseur de type.

Livrables

Vous pouvez dupliquer le projet GitHub pour modifier le code source du projet et fournir un projet qui compile.

En résumé

  • L’annotation@Entitypermet de désigner une classe qui sera stockée dans une base de données et permet également de définir des relations entre plusieurs classes en précisant les contraintes d’intégrité référentielle qui les lient.

  • L’annotation @PrimaryKeypermet d’indiquer l’attribut d’une entité qui sert de clé primaire pour la table de la base de données associée.

  • L’annotation@ColumnInfopermet de personnaliser les informations de la colonne de la base de données pour un champ donné ;

  • L’annotation@Embedded permet d’indiquer que les champs d’une autre classe doivent être inclus dans la table de la base de données de l’entité.

  • L’annotation@TypeConverterpermet de créer des convertisseurs de types pour convertir des types de données personnalisés en types de données pris en charge par SQLite.

Les entités de l’application PETiSoin sont bien annotées. Nous pouvons donc continuer notre exploration de Room pour définir les opérations autour de la base de données de l’application PETiSoin.

Exemple de certificat de réussite
Exemple de certificat de réussite