• 8 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 2/2/22

Manipulez vos données grâce aux DAO

Maintenant que nos entités sont définies, il serait intéressant de pouvoir les manipuler à travers notre base de données SQLite. Par exemple, nous aimerions pouvoir ajouter une chose à faire dans notre base, la mettre à jour ou encore la supprimer... Bref, réaliser les différentes actions CRUD !

Eh bien, c'est ce que nous allons faire immédiatement grâce à un patron de conception (ou Design Pattern en anglais) appelé Data Access Object (DAO), en français, un objet d'accès aux données.

Euh, attends, c'est quoi un design pattern ? Pourquoi tu nous parles de DAO tout d'un coup ? :'(

Bonne question ! Un Design Pattern est un modèle (ou un ensemble de bonnes pratiques logicielles) destiné à aider les développeurs à concevoir la meilleure solution face à un même problème logiciel. On ne réinvente jamais la roue !

Dans notre cas, nous avons un problème : quelle est la meilleure façon d'accéder aux données se trouvant dans notre base de données SQLite ? Autrement dit, l'approche la plus "propre" en programmation orientée objet ?

 

Eh bien, c'est là que le DAO apparaît ! :D Ce pattern nous propose de regrouper les accès aux données persistantes dans des classes à part, plutôt que de les disperser. Et si on appliquait cela à nos deux tables   User  et   Item  ?

Ainsi, créez un package appelé database/ puis un sous package dao/ et placez-y les deux interfaces (attention, pas des classes, hein... ) suivantes :   ItemDao  et   userDao  .

Interface database/dao/ItemDao.java :

@Dao

public interface ItemDao {

   @Query("SELECT * FROM Item WHERE userId = :userId")

   LiveData<List<Item>> getItems(long userId);

   @Query("SELECT * FROM Item WHERE userId = :userId")

   Cursor getItemsWithCursor(long userId);

   @Insert

   long insertItem(Item item);

   @Update

   int updateItem(Item item);

   @Query("DELETE FROM Item WHERE id = :itemId")

   int deleteItem(long itemId);

}

 Explications : Room nous impose un format particulier pour créer un DAO en nous demandant de créer une interface par DAO. Ici, nous avons créé l'interface   ItemDao  dont l'objectif et la responsabilité seront de regrouper toutes les actions CRUD pour la table  Item  . 

Afin d'indiquer l'interface comme étant une classe DAO, nous avons ajouté l'annotation@Daoen haut de celle-ci. Puis nous avons créé les 4 actions CRUD via les méthodes suivantes :

  • getItems()  : permet de récupérer la liste des choses à faire (Item) pour un utilisateur. Nous avons utilisé l'annotation@Querypermettant de définir la méthode comme étant une requête SQL. Nous retournons une liste d'Item de type LiveData, que j'expliquerai en détail dans la prochaine partie de ce cours.  

  • getItemsWithCursor()  :  permet de récupérer la liste des choses à faire (Item) pour un utilisateur. Cette fois-ci nous récupérons un Cursor et non un LiveData de liste d’Item. Le cursor fournit un accès à l’ensemble du résultat renvoyé par la base de données.

  • insertItem()  : permet de créer une nouvelle chose à faire (Item) grâce à l'annotation@Insert. Notez que nous lui avons passé un objet Item directement en paramètre de la méthode. Cet objet n'aura pas besoin d'avoir un identifiant défini, car rappelez-vous du précédent chapitre, Room le générera pour nous !

  • updateItem()  : permet de mettre à jour une chose à faire existante (Item) grâce à l'annotation @Update. Notez également que nous lui avons passé un objet Item directement en paramètre. Cet objet devra absolument avoir un identifiant défini, afin que Room puisse le retrouver dans la BDD et le mettre à jour. ;)

  • deleteItem()  : permet de supprimer une chose à faire existante (Item) en BDD. Notez ici que nous avons réutilisé l'annotation   @Query  car nous avions besoin de créer une requête SQL un peu poussée. Vous avez également la possibilité de passer directement un objet Item et d'utiliser l'annotation @Delete  pour le supprimer.

Interface database/dao/UserDao.java :

@Dao

public interface UserDao {

   @Insert(onConflict = OnConflictStrategy.REPLACE)

   void createUser(User user);

   @Query("SELECT * FROM User WHERE id = :userId")

   LiveData<User> getUser(long userId);

}

 Explications : Nous avons ici défini l'interface DAO dédiée à la manipulation de la table   User  . Nous avons ajouté seulement deux méthodes : Une pour créer un nouvel utilisateur (   createUser  ) et une pour récupérer un utilisateur (  getUser  ). Vous remarquerez que nous avons ajouté à l'annotation   @Insert  , le paramètre   onConflict = OnConflictStrategy.REPLACE  permettant d'écraser un utilisateur déjà existant possédant le même ID que celui que l'on souhaite insérer.

Et c'est tout ! :D Enfin, il nous reste tout de même à configurer une classe très importante, dont le rôle sera de lier toutes les classes/interfaces que nous avons précédemment créées ensemble, et surtout de configurer notre base de données !

Ainsi, je vous laisse créer la classe SaveMyTripDatabase dans le package database/.

Classe database/SaveMyTripDatabase.java :

@Database(entities = {Item.class, User.class}, version = 1, exportSchema = false)

public abstract class SaveMyTripDatabase extends RoomDatabase {

   // --- SINGLETON ---

   private static volatile SaveMyTripDatabase INSTANCE;

   // --- DAO ---

   public abstract ItemDao itemDao();

   public abstract UserDao userDao();

   // --- INSTANCE ---

   public static SaveMyTripDatabase getInstance(Context context) {

       if (INSTANCE == null) {

           synchronized (SaveMyTripDatabase.class) {

               if (INSTANCE == null) {

                   INSTANCE = Room.databaseBuilder(context.getApplicationContext(),

                           SaveMyTripDatabase.class, "MyDatabase.db")

                           .addCallback(prepopulateDatabase())

                           .build();

               }

           }

       }

       return INSTANCE;

   }

   private static Callback prepopulateDatabase() {

       return new Callback() {

           @Override

           public void onCreate(@NonNull SupportSQLiteDatabase db) {

               super.onCreate(db);

               Executors.newSingleThreadExecutor().execute(() -> INSTANCE.userDao().createUser(new User(1, "Philippe", "https://oc-user.imgix.net/users/avatars/15175844164713_frame_523.jpg?auto=compress,format&q=80&h=100&dpr=2")));

           }

       };

   }

}

 Explications : Nous créons ici une classe abstraite, héritant de RoomDatabase  et étant définie par l'annotation @Database. Cette même annotation répertoriera les différentes tables (appelées "entities").

À l'intérieur de cette classe, nous avons déclaré nos deux interfaces de DAO. Puis, nous avons créé une méthode,   getInstance()  , permettant de créer un singleton de notre classe SaveMyTripDatabase.

Euuuuuh, c'est quoi un singleton ? :o

Eh bien, c'est encore un design pattern... ;) En fait, nous rencontrons ici malgré nous un problème logiciel : comment créer une seule fois la classe responsable de notre base de données et n'obtenir qu'une seule et unique instance de référence ? 

Ah... Mais pourquoi a-t-on besoin d'une seule instance de notre classe gérant notre base de données ?

Tout simplement car si vous avez plusieurs instances de votre base de données, vous autorisez potentiellement plusieurs instances à manipuler l'unique fichier de votre base en même temps ! Si deux instances ouvrent le fichier de SQLite et qu'elles tentent de le modifier en même temps, cela risque de poser un léger problème, non ?  

C'est donc pour cette raison que nous créons un Singleton ! Ce dernier créera un nouvel objet RoomDatabase grâce à son builderRoom.databaseBuilderet créera un fichier qui contiendra notre base de données SQLite. Si jamais cette méthode est rappelée par la suite, nous renverrons uniquement la référence de notre base de données.

Aussi, comme vous pouvez le voir, nous avons rajouté la méthode addCallback  à notre builder, qui nous permettra de remplir celle-ci avec un utilisateur de test grâce à la méthode que nous avons créée juste en dessous,  prepopulateDatabase  .

En résumé

  • Les DAO permettent de lister les différentes manipulations possibles de nos tables  via des requêtes.

  • Nous utilisons un singleton de notre base de données afin de ne pas avoir de problèmes de concurrence d’accès.

Et voilà ! Notre base de données SQLite est correctement configurée grâce à Room. Il ne nous reste plus qu'à essayer tout cela... Et c'est ce que nous allons faire dans le prochain chapitre !  ;)

Example of certificate of achievement
Example of certificate of achievement