Data Access Objet (DAO)

TP 4
Author

Rémi Pépin, Ludovic Deneuville

Avant de commencer

😱 Comme vous pouvez le constater le sujet de ce TP est lui aussi long. Cela ne doit pas vous effrayer. Il mélange explications complètes et manipulations pour être au maximum autosuffisant. Vous n’allez surement pas terminer le sujet, ce n’est pas grave. Il est là pour vous aider lors du projet informatique.

Ce TP mêle explications pour vous faire comprendre ce qui est fait, et phases de manipulation ou code.

Les explications de ce TP ne doivent pas prendre le pas sur celles de votre intervenant. Prenez-les comme une base de connaissances pour plus tard, mais préférez toujours les explications orales.

Objectifs

Dans ce TP vous allez :

  • Revoir des notions de base de données relationnelles
  • Implémenter le patron de conception DAO
  • Voir si votre programme fonctionne avec des tests unitaires reproductibles

1 Mise à jour de votre dépôt local

Caution

Si vous n’avez pas le dépôt sur votre machine, créez un clone en suivant la section Clone du dépôt du TP2.

    • File > Open Folder
    • Allez dans le dossier P:\Cours2A\UE3-Complements-info\TP2
    • Cliquez une seule fois sur 📁 ENSAI-2A-complement-info-TP
    • Puis sur le bouton Sélectionner un dossier
Important
    • L’Explorer, à gauche, permet d’explorer l’arborsence des fichiers et dossiers

⚠️ Si le dossier parent n’est pas le bon, recommencez l’Open Folder où vous aurez de gros soucis par la suite !!!

    • Terminal > New Terminal (ou CTRL + ù)
    • Passez sur la branche du TP3 et mettez-là à jour
      • si vous ne l’avez pas fait à la fin du dernier TP, commencer par un add et un commit
      • git checkout tp4-base
      • git pull
Important

Pour pour vérifier que tout fonctionne :

    • dans le terminal : python -m pytest -v src/tests/test_business_object/

S’il vous manque des packages, suivez les instructions du README.

1.1 Connexion à la BDD

    • DATABASE=id????
    • USER=id????
    • PASSWORD=id????

Maintenant que la connexion est opérationnelle, initialisez votre base de données :

Ce script crée les tables et insére des données.

1.2 Remise en jambes SQL

Écrivez des requêtes pour :

Gardez ces requêtes de coté, elles seront utiles plus loin.

2 Data Access Objet (DAO)

2.1 Modélisation

Reprenons le diagramme de classe du TP2. Limitons nous à la partie “attaque” et réfléchissons où mettre une méthode qui permet de persister les attaques.

classDiagram

class AbstractAttack{
    <<abstract>>
    + DATABASE_TYPE_LABEL : str
    # _id : int
    # _power : int
    # _name : str
    # _description : str
    +compute_damage(APkm, APkm)$  int
    }
    class FixedDamageAttack{
        + compute_damage(APkm,APkm )  int
    }
    class AbstractFormulaAttack{
    <<abstract>>
        -get_attack_stat(APkm)$  float
        -get_defense_stat(APkm)$  float
        +compute_damage(APkm,APkm)  int
    }
    
    class PhysicalAttack{
        -get_attack_stat(APkm)  float
        -get_defense_stat(APkm)  float
    }
    
    class SpecialAttack{
        -get_attack_stat(APkm)  float
        -get_defense_stat(APkm)  float
    }
    
    FixedDamageAttack--|>AbstractAttack
    AbstractFormulaAttack--|>AbstractAttack
    SpecialAttack--|>AbstractFormulaAttack
    PhysicalAttack--|>AbstractFormulaAttack


Vu que les attributs de nos attaques sont similaires, nous n’allons pas coder cela dans les classes spécifiques des attaques. Nous pourrions mettre les méthodes dans AbstractAttack. Ça fonctionnerait bien d’ailleurs. Nous aurions une classe unique avec nos méthodes pour interagir avec la base. Mais nous n’allons pas faire ça !

Et là vous vous demandez :

😱 Mais pourquoi ???

Et la réponse est :

😛 Car ça n’a aucun sens !

Revenons sur la phrase : faible couplage, forte cohésion. Si nous mettons toutes les méthodes de persistance de nos attaques dans la classe AbstractAttack, nous allons avoir une classe qui :

  • ✔️ Détermine le comportement des attaques. C’est exactement ce que l’on souhaite (forte cohésion).
  • Détermine comment on persiste une attaques.
Important

Mais ça, ce n’est pas de la responsabilité d’une attaque, mais du système de persistance choisi, ou du moins de l’intermédiaire entre nos objets et le système de persistance !

Je n’ai personnellement pas envie d’aller modifier ma classe AbstractAttack uniquement car j’ai décidé de changer de système de gestion de la persistance. Je risque de modifier quelque chose que je ne devrais pas et créer des régressions (faire apparaitre des erreurs sur un code qui n’en avait pas avant) dans mon code. Or j’aimerais bien limiter les sources d’erreurs.

À la place, nous allons créer une classe qui va s’occuper uniquement de cette tâche : une classe DAO pour Data Access Object. C’est une classe technique qui va faire l’interface entre nos données stockées et notre application. Voilà ce que cela donne en terme de diagramme de classe.

classDiagram

class AbstractAttack{
    <<abstract>>
    + DATABASE_TYPE_LABEL : str
    # _id : int
    # _power : int
    # _name : str
    # _description : str
    +compute_damage(APkm, APkm)$  int
    }
    class FixedDamageAttack{
        + compute_damage(APkm,APkm )  int
    }
    class AbstractFormulaAttack{
    <<abstract>>
        -get_attack_stat(APkm)$  float
        -get_defense_stat(APkm)$  float
        +compute_damage(APkm,APkm)  int
    }
    
    class PhysicalAttack{
        -get_attack_stat(APkm)  float
        -get_defense_stat(APkm)  float
    }
    
    class SpecialAttack{
        -get_attack_stat(APkm)  float
        -get_defense_stat(APkm)  float
    }
    
    FixedDamageAttack--|>AbstractAttack
    AbstractFormulaAttack--|>AbstractAttack
    SpecialAttack--|>AbstractFormulaAttack
    PhysicalAttack--|>AbstractFormulaAttack


class AttackDao{
<<Singleton>>
 +create(AbstractAttack) AbstractAttack
 +find_by_id(int) AbstractAttack
 +find_all() List[AbstractAttack]
 +update(AbstractAttack) AbstractAttack
 +delete(AbstractAttack) bool
}

class DBConnection{
<<Singleton>>
-__connection : Connection

+connection() Connection
}

AbstractAttack<.. AttackDao: create
AttackDao..> DBConnection: use

2.2 Gestion des connexions et patern singleton

Pour vous connecter à la base de données nous allons utiliser la bibliothèque python psycopg2. C’est elle qui va établir la connexion avec la base, envoyer nos requêtes et nous retourner les résultats.

Mais il faut faire un peu attention à la gestion des connexions. Car nous pourrions nous retrouver à ouvrir des centaines de connexions rapidement et dégrader les performances de notre application. C’est le travail de la classe DBConnection. Comme c’est un singleton, il y aura une seule instance de cette classe dans toute notre application, et comme c’est elle qui se connecte à la base, nous nous assurons de l’unicité de la connexion.

Tip

Cette classe est une solution purement technique, alors n’hésitez pas à la réutiliser pour votre projet. Elle introduit un concept avancé de POO, à savoir les méta classes.

Une méta classe permet de modifier le comportement d’une classe à un niveau poussé (par exemple modifier comment les objets sont construits par python). À moins que vous ayez une appétence tout particulière pour l’informatique, ne passez pas de temps sur ce sujet.

2.3 DAO et CRUD

Si vous faites attention, les méthodes de notre DAO ressemblent à celles du CRUD. C’est normal car c’est dans ces méthodes que le code SQL va être stocké.

Les méthodes de base sont généralement :

  • find_all() : qui va retourner toute la table.
  • find_by_id() : qui retourne un enregistrement à partir de son id
  • create() : qui crée un nouvel enregistrement
  • delete() : qui supprime un enregistrement
  • update() : qui met à jour un enregistrement

Ces 5 méthodes suffisent pour communiquer avec votre base de données. Vous pouvez effectuer le reste des traitements dans vos classes Service. Néanmoins pour gagner du temps rien n’empêche de créer des méthodes plus complexes (ex : find_by_type_and_level_order_by_name_desc())

Voici la fonctionnement général d’une des méthodes de la DAO :

# Etape 1 : On récupère une connexion en utilisant la classe DBConnection.
with DBConnection().connection as connection :

# Etape 2 : à partir de la connexion on fait un curseur pour la requête 
    with connection.cursor() as cursor : 
    
    # Etape 3 : on exécute notre requête SQL.
            cursor.execute(requete_sql)
    
    # Etape 4 : on stocke le résultat de la requête
            res = cursor.fetchall()

if res:
    # Etape 5 : on agence les résultats selon la forme souhaitée (liste...)
    
return something

L’objet cursor contient un pointeur vers les résultats de votre requête. Ce résultat n’est pas encore rapatrié sur votre machine, mais est stocké par la base de données. Vous avez 3 méthodes pour récupérer le résultat :

  • cursor.fetchone() : retourne uniquement un enregistrement sous forme de dictionnaire
    • Si vous appelez de nouveau fetchone() sur le même curseur vous obtiendrez la ligne suivante
  • cursor.fetchall() : retourne l’intégralité des résultats sous forme d’une liste de dictionnaires
    • Les dictionnaires sont les lignes de la table récupérée.
    • Les clés du dictionnaire sont les colonnes récupérées.
    • Cette méthode fonctionne très bien pour avoir tous les résultats en une fois et qu’il y en a peu. Quand on a des millions d’enregistrements cela va poser problème car :
      • Le transfert de données sur internet va prendre du temps et bloquer notre application
      • Notre application va devoir gérer une grande quantité de données, et elle en est peut-être incapable
  • cursor.fetchmany(size): retourne autant d’enregistrements que demandé sous forme d’une liste de dictionnaires. Cela permet de contrôler le volume de données que l’on traite. Si vous appelez de nouveau fetchmany(size) sur votre curseur, vous allez récupérer les lignes suivantes (système de pagination)

Pour plus d’information : article de pynative

3 Premières DAO

3.1 DAO avec des types d’attaques

    • cela va vous être utile pour la suite

Dans la classe AttaqueDao, créez les méthodes suivantes :

Quelques conseils
  • Vous pouvez utiliser l’attribut type de chaque attaque pour avoir son label en base
  • Utilisez la classe AttackFactory pour instancier facilement des attaques
  • Pensez à faire des tests pour voir si votre code fonctionne
  • Dans les 2 méthodes find, pour créer nos objets métier Attack
    • nous avons besoin de connaitre le nom du type d’attaque
    • or il n’y a pas cette colonne dans la table attaque
    • mais peut-être avez-vous déjà une requête qui fait le job ?

3.2 Pokémon DAO

Créez la classe PokémonDAO avec les méthodes suivantes :

    • Faites une requête en joignant les tables attack et pokemon_attack en filtrant avec l’id du pokémon
    • Générez les attaques à partir de là

4 DAO et webservice

Vous allez maintenant rendre accessible les données de votre base à d’autres utilisateurs en réalisant un webservice REST.

Ajoutez dans le fichier app.py les endpoints suivants :

# Défintion du endpoint get /attack?limit=100
@app.get("/attack/")
async def get_all_attacks(limit:int):
    # Vous devez récupérer les attaques en base en utilisant votre DAO
    return attacks

# Défintion du endpoint get /pokemon?limit=100
@app.get("/pokemon/")
async def get_all_pokemons(limit:int):
    # Vous devez récupérer les pokemons en base en utilisant votre DAO
    return pokemons

# Défintion du endpoint get /pokemon`/{name}
@app.get("/pokemon/{name}")
async def get_pokemon_by_name(name:str):
    # Vous devez récupérer le pokemon en base en utilisant votre DAO
    return pokemon
  • GET localhost:80/attack?limit=100 renverra une liste de 100 attaques par défaut. Il est possible de moduler cette valeur via le paramètre de requête limit
  • GET localhost:80/pokemon?limit=100. Il renverra une liste de 100 Pokemon par défaut, mais peut être modulé avec le paramètre de requête limit.
  • GET localhost:80/pokemon/{nom}. Il renverra un json représentant un Pokemon.

Pour retourner des objets, vous allez devoir définir des classes héritant de BaseModel. Vous trouverez toutes les infos dans la documentation de FastAPI.

Conclusion

Dans ce TP vous avez implémenté votre première DAO.

C’est une classe technique qui sert à communiquer avec votre système de persistance de données. L’avantage premier de faire une classe à part est de découpler au maximum la gestion du système de persistance et le code métier de votre application.

Si vous décidez d’arrêter d’utiliser une base de données relationnelle et préférez désormais une base de données NoSQL vous allez devoir changer uniquement les classes DAO tout en exposant toujours les mêmes méthodes.