Data Access Objet (DAO)
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.
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
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
-
- 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 TP4 et mettez-là à jour
- si vous ne l’avez pas fait à la fin du dernier TP, commencez par un add et un commit
git switch tp4-base
git pull
Pour pour vérifier que tout fonctionne :
-
- dans le terminal :
python -m pytest -v src/tests/test_business_object/
- dans le terminal :
S’il vous manque des packages, suivez les instructions du README.
1.1 Connexion à la BDD
-
.env
POSTGRES_HOST=sgbd-eleves.domensai.ecole POSTGRES_PORT=5432 POSTGRES_DATABASE=idxxxx POSTGRES_USER=idxxxx POSTGRES_PASSWORD=idxxxx
- N’oubliez pas de remplacer xxxx par votre id
Maintenant que la connexion est opérationnelle, initialisez votre base de données :
Ce script va exécuter 2 fichiers sql :
- data/init_db.sql pour créer les tables
- data/pop_db.sql pour insérer des données
2 Remise en jambes SQL
Sur le réseau ENSAI, le nombre de connexions à votre base de données est limité.
Or par défaut, DBeaver utilise déjà 3 connexions, ce qui risque de vous poser des soucis lorsque vous essaierez de vous connecter depuis votre programme Python.
La solution :
Écrivez les 3 requêtes suivantes (elle serviront pour la suite du TP) :
Pour vous aider, voici le schéma des tables et leurs relations
3 Data Access Objet (DAO)
3.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.
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 attaque.
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 (i.e. faire apparaitre des erreurs sur un code qui n’en avait pas avant). 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 (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.
3.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.
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 toute particulière pour l’informatique, ne passez pas de temps sur ce sujet.
3.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 idcreate()
: qui crée un nouvel enregistrementdelete()
: qui supprime un enregistrementupdate()
: 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 (conseillé). Cependant, rien ne vous 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 :
une_classe_dao.py
from dao.db_connection import DBConnection
class UneClasseDAO(metaclass=Singleton):
def une_methode_dao():
# Etape 1 : On récupère la connexion en utilisant la classe DBConnection.
with DBConnection().connection as connection :
# Etape 2 : à partir de la connexion on crée 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
= cursor.fetchall()
res
if res:
# Etape 5 : on agence les résultats selon la forme souhaitée (objet, 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)
Exemples de données retournées (requête du curseur : SELECT * FROM tp.attack;
):
Un dictionnaire
"id_attack": 2, "id_attack_type": 3, "power": 20, "accuracy": 100, "element": "Grass", "attack_name": "Absorb"} {
Une liste de dictionnaires
["id_attack": 2, "id_attack_type": 3, "power": 20, "accuracy": 100, "element": "Grass", "attack_name": "Absorb"},
{"id_attack": 3, "id_attack_type": 2, "power": 40, "accuracy": 100, "element": "Rock", "attack_name": "Accelerock"},
{"id_attack": 4, "id_attack_type": 3, "power": 40, "accuracy": 100, "element": "Poison", "attack_name": "Acid"},
{
... ]
Pour plus d’informations : article de pynative
Comme pour les données des webservices, l’objectif va être de convertir ces données en objets métiers.
4 Premières DAO
4.1 DAO avec des types d’attaques
-
- cela va vous être utile pour la suite
Dans la classe AttackDao, créez les méthodes suivantes :
-
- Bonus : ajoutez à cette méthode les paramètres limit et offset
- 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 ?
- Utilisez la classe AttackFactory pour instancier facilement des objets métier Attack
- Pensez à faire des tests pour voir si votre code fonctionne
4.2 Pokémon DAO
Créez la classe PokémonDAO avec les méthodes suivantes :
-
- Faites une requête en joignant les tables
attack
etpokemon_attack
en filtrant avec l’id du pokémon - Générez les attaques à partir de là
- Faites une requête en joignant les tables
5 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):
# TODO: 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):
# TODO: 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):
# TODO: 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êtelimit
GET localhost:80/pokemon?limit=100
. Il renverra une liste de 100 Pokemons par défaut, mais peut être modulé avec le paramètre de requêtelimit
.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.
Si vous souhaitez voir la version finale, positionnez-vous sur la branche tp5-correction
et lancez le main.