🛢 Persistence des données

Lorsque l’on execute un programme, il utilise une mémoire qui lui est allouée pour pouvoir fonctionner. A la fin de l’execution du programme la mémoire est désallouée et donc tout ce qui était dans le contexte d’execution du programme n’est plus accessible.

C’est ainsi que vient la notion de persistence des données qui se présente comme la nécessité de pouvoir sauvegarder les résultats et états du système pour conserver des éléments déjà traités.

En général, on sépare le stockage des données d’un programme entre des fichiers et des bases de données.

Comme pour les autres parties, les exemples sont disponibles ici : https://github.com/conception-logicielle-ensai/exemples-cours/tree/main/cours-5/persistence

Pour aller plus loin

Un site qui vulgarise l’execution du code python côté machine https://pythontutor.com/python-compiler.html#mode=edit

Pour aller plus loin

Petite vidéo sur le fonctionnement stack vs heap : https://www.youtube.com/watch?v=5OJRqkYbK-4

Fichiers

Les fichiers peuvent avoir différents formats pour stocker de la donnée

  • les fichiers au format de texte: csv, txt, xml, json, yaml
  • les fichiers au format binaire : parquet, zip, …

Les fichiers peuvent être ensuite importés ou exportés depuis le code, exemple avec du json:

import json

# Données à sauvegarder
data = {"utilisateurs": [{"nom": "Alice", "age": 30}, {"nom": "Bob", "age": 25}]}

# Écriture dans un fichier JSON
with open("data.json", "w") as f:
    json.dump(data, f, indent=4)

# Lecture du fichier JSON
with open("data.json", "r") as f:
    data = json.load(f)
    print(data)

Ces fichiers sont ensuite stockés directement auprès du serveur qui execute le processus en local (par exemple votre poste de travail quand vous êtes en local) et peuvent être synchronisé et stockés dans des espaces adaptés.

Par exemple, on pourrait vouloir, a la fin d’un processus sauvegarder les fichiers construits par nos traitement dans des services de stockage de fichier de type S3, ces services permettant d’héberger a faible coût et avec facilité d’accès, des fichiers non structurés.

Pour aller plus loin
Stockage objet, client s3 python : boto3
Pour aller plus loin

Documentation d’utilisation du stockage S3 sur les espaces du datalab sspcloud : https://docs.sspcloud.fr/content/storage.html

Systèmes de gestion de base de données (SGBD)

Dans cette partie nous nous interesserons spécifiquement aux bases de données relationnelles et document.

Bases de données relationnelles

Les bases de données de données relationnelles sont des bases dans lesquelles les données sont organisées sous forme de table. On les préconise dans des cas où l’architecture sous tend une structure des données fixe.

✅ Avantages :

  • Cohérence et intégrité des données grâce aux relations.
  • Bonne optimisation pour les requêtes complexes.

❌ Inconvénients :

  • Peu flexible : toute modification de la structure nécessite des migrations.
  • Moins performant pour des données non structurées ou fortement imbriquées.

Exemples : MySQL, SQLite, PostgreSQL, H2

Bases de données orientées document

Les bases de données document permettent le stockage de données sous des formats flexibles, a base de document généralement en JSON ou BSON (compressé). Les documents n’ont pas a respecter un schéma propre et puisqu’il n’y a pas de contraintes, les bdd document sont optimisées pour des requêtes sur le contenu des documents.

✅ Avantages :

  • Données non structurées : on peut modifier la structure des données facilement.
  • Performant pour les applications à forte scalabilité (ex : applications web, big data) .
  • Idéal pour stocker des données complexes (ex : profils utilisateurs avec paramètres variés, nullables).

❌ Inconvénients :

  • Moins adaptées aux transactions complexes et aux relations entre les données.
  • Peut poser des problèmes d’intégrité des données en raison du manque de normalisation.

Exemples : MongoDB, CouchDB (accessible en via api rest)…

Remarque : Lors de vos choix de conception , il faudra privilégier une base de données cohérente avec votre architecture si vous avez besoin de persister des données en base de données

Fiabilité et Performance des Bases de Données

Transactions et ACID

Les bases de données relationnelles implémentent les principes ACID (Atomicité Cohérence Isolation Durabilité) pour leurs transactions.

Remarque: Une transaction comment par un BEGIN et finit au COMMIT. On peut donc réaliser plusieurs modifications dans une même transaction.

Atomicité

Une transaction est toujours tout ou rien

Exemple:

Dans le cas d’un transfert bancaire, si il y a un dysfonctionnement il n’y aura pas de situation ou une personne reçoit de l’argent que l’autre personne a encore dans son compte en banque.

Cohérence

Une transaction assure que les données transférées ne sont pas corrompues ou respectent des règles fixées dans le modèle (pas d’identifiant null…)

Cela permet de stopper un traitement en cas de corruption et de rollback

Isolation

Les transactions s’executent indépendamment les unes des autres.

Cela permet d’éviter les conflits

Durabilité

Les données, après la transaction sont bien persistées même si il y a une panne.

Applications aux transactions au niveau du code python

On peut donc effectuer plusieurs modification au sein d’une même transaction, exemple (avec psycopg2).

import psycopg2

try:
    # il faut executer le script rollback-initdb.sql pour tester
    conn = psycopg2.connect(dbname="postgres", user="postgres", password="postgres", host="localhost")
    cursor = conn.cursor()
    cursor.execute("BEGIN")
    cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("Alice", 25))
    cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("Bob", 35))
    conn.commit()
except Exception as e:
    conn.rollback()
    print("Erreur :", e)
finally:
    cursor.close()
    conn.close()

MongoDB implémente également ce principe, cela assure une bonne gestion des évolutions des données.

Formes normales

Les formes normales décrivent des règles qui assurent la bonne organisation des données et évitent la redondance.

  • 1ère forme normale (1NF) : Chaque colonne contient des valeurs atomiques, et chaque ligne est unique.

Il faut donc ici ne pas mettre de valeurs qui contient une liste de valeurs, on privilégie de mettre en place

  • 2ème forme normale (2NF) : Respecte la 1NF et assure qu’il n’y a pas de dépendances partielles.

Il ne faut pas que des champs soient interdépendants entre eux exactement. (un champ ne doit pas être en correlation totale avec un autre champ)

  • 3ème forme normale (3NF) : Respecte la 2NF et supprime les dépendances transitives.

Il faut ici que les champs dans nos tables soient non correlés des champs que l’on pourrait retrouver sans jointure directe par des clés dans une autre table.

Soit encore :

Il faut ici que les informations dans une table soient directement liées à la clé primaire de cette table, et non à une colonne non-clé (2NF). Les colonnes non-clé ne doivent pas dépendre les unes des autres, et toute relation entre les informations d’autres tables doit se faire via des clés étrangères et non par des dépendances directes entre colonnes non-clé (3NF).

Exemple

Voici un exemple présentant une table qui respecte la 1ère forme normale mais pas la 2ème.

CREATE TABLE students_courses (
    student_id INT,
    student_name VARCHAR(100),
    course_name VARCHAR(100),
    course_instructor VARCHAR(100),
    PRIMARY KEY (student_id, course_name)
);

INSERT INTO students_courses (student_id, student_name, course_name, course_instructor) 
VALUES
(1, 'Bob', 'Math', 'Dr. Smith'),
(1, 'Bob', 'Physics', 'Dr. Brown'),
(2, 'Alice', 'Math', 'Dr. Smith'),
(2, 'Alice', 'Biology', 'Dr. White');
  • On voit bien ici qu’il y a atomicité des données ligne a ligne, par contre on repète les informations dépendantes : le cours et le professeur sont correlés.

Il faut donc découper en 2 tables avec une table d’association, pour isoler les données venant du cours, des données des élèves.

CREATE TABLE students (
    student_id INT PRIMARY KEY,
    student_name VARCHAR(100)
);

CREATE TABLE courses (
    course_id INT PRIMARY KEY,
    course_name VARCHAR(100),
    course_instructor VARCHAR(100)
);

CREATE TABLE student_courses (
    student_id INT,
    course_id INT,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(student_id),
    FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
INSERT INTO students (student_id, student_name) VALUES
(1, 'Bob'),
(2, 'Alice');

INSERT INTO courses (course_id, course_name, course_instructor) VALUES
(1,'Math', 'Dr. Smith'),
(2,'Physics', 'Dr. Brown'),
(3,'Biology', 'Dr. White');

INSERT INTO student_courses (student_id,course_id) VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 3);

Performances

Les bases de données relationnelles reposent leurs optimisations sur des fonctionnalités de calcul de statistiques Analyze. Elle suivent ensuite des plans d’executions selon les données collectées et précalculées sur la bases de données.

Pour les performances on peut être amenés a utiliser les méthodes suivantes :

  • Indexation : On définit une clé homogène pour accèder a des données - équivalent de précalculer une clause where par exemple
  • Dénormalisation : On copie les données dans une autre table plus petite pour avoir moins de colonnes a récupérer, cela permet d’accéder plus vite aux informations.
  • Vues: On précalcule un ensemble de données que l’on peut ensuite cacher côté BDD.
Pour aller plus loin

Instagram et la dénormalisation : Le Justin Bieber Problem : https://medium.com/@AVTUNEY/how-instagram-solved-the-justin-bieber-problem-using-postgresql-denormalization-86b0fdbad94b

Implémentations

SQL - Base de données embarquée, exemple avec SQLite

SQLite

est un système de base de données embarqué. A la différence des systèmes de base de données classiques (type postgresql, oracle, mysql …), l’architecture de SQLite ne contient pas de serveur. C’est l’application elle même qui écrit et lit les données à partir d’un fichier .sqlite contenant l’ensemble des données.
SQLite est un moteur de base de données fantastique, utilisé dans des milliards d’appareil (vous venez de regarder vos SMS ? Votre smartphone a utilisé SQLite. Vous venez de regarder le ciel ? Le satellite que vous voyez a très probablement des bases de données sqlite en lui). Si le sujet vous intéresse, cette conférence est formidable : https://www.youtube.com/watch?v=gpxnbly9bz4

Pour nous, le principal avantage de SQLite va venir de son architecture simplifiée, il n’y a pas besoin de faire tourner de base de données indépendante.

Tutorial d’utilisation de sqlite :
https://www.digitalocean.com/community/tutorials/how-to-use-the-sqlite3-module-in-python-3

Bases de données documentaires (NoSQL), exemple avec mongodb

Les bases de données NoSQL permettent de traiter des collections de données non structurées. Elles ont de nombreux avantages.

MongoDB 🍃 est un leader dans le monde des bases de données NoSQL, il permet de stocker et de traiter des calculs sur de nombreux documents. L’intêret de ce type de base de données est dans l’utilisation de fonctionnalités avancées pour le calcul :

  • Le sharding : MongoDB prend en charge la scalabilité horizontale, c’est-à-dire la possibilité de répartir les données sur plusieurs serveurs (shards).
  • Haute disponibilité : par sa scalabilité on peut également bénéficier de transactions rapidement en cas de montée de charge.
  • Indexation: Comme les autres BDD, mongoDB permet l’indexation des données, qui, précalculées, sont donc récupérables très efficacement.

On peut interagir avec des moteurs permettant la connexion aux bases de données mongo, en python on privilégiera l’utilisation de pymongo.

Exemple de code : https://github.com/conception-logicielle-ensai/exemples-cours/blob/main/cours-5/persistence/mongodb/mongo_exemple.py

MongoDB fonctionne avec des Collections équivalent des tables. Dans celle ci on peut déposer des données non structurée :

# En abstrayant la connexion a la bdd
COLLECTION = "user_data"
collection = db[COLLECTION]  # Remplacez par le nom de votre collection

# Exemple d'insertion d'un document dans la collection
document = {
    "nom": "John",
    "age": 30,
    "ville": "Paris"
}
collection.insert_one(document)

Object Relationnal Mapping

L’Object-Relational Mapping (ORM) est une technique qui permet de mapper des objets du langage de programmation orienté objet a des tables en base de données.

Il s’agit donc d’utiliser les fonctionnalités de POO de python pour sérialiser/desérialiser automatiquement les données récupéréesen base de données.

L’ORM propose également une interface de haut niveau qui permet de s’abstraire du type de base de données utilisées, bien qu’on utilise cela en général dans des bases de données a la structure stable, des bases de données relationnelles.

Il propose finalement une interface fonctionnelle python pour ne plus avoir a développer les scripts d’accès a la base de données et permet de travailler avec des bases de données de language différentes a partir du même code python.

Par exemple:

  • En local on voudrait travailler sur une base de données portable : SQLite, mysql
  • Dans un environnement hébergé on voudrait travailler sur une base de données : postgresql

SQLAlchemy

SQLAlchemy est une implémentation proposant une interface d'Object Relationnal Mapping en Python. Il implémente de nombreuses fonctionnalités dont : - La gestion des relations - La gestion des injections SQL - La gestion des connexions (pour éviter des fuites de connexion)

Il fait cela au travers de ce qu’on appelle un engine qui lui même s’appuie sur des connecteurs.

  • En python il s’agira des connecteurs permettant de se connecter aux bases habituellement : psycopg2 mysql sqlite3
  • Dans le monde java on utilise l’interface jdbc et des implémentations en fonction du type de bases de données : org.postgresql

La syntaxe se présente comme suit :

On utilise le module declarative_base pour rendre nos classes mappées

Base = declarative_base()

# Définition du modèle pour la table "Utilisateur"
class Utilisateur(Base):
    __tablename__ = 'utilisateurs'
    
    id = Column(Integer, primary_key=True)
    nom = Column(String)
    age = Column(Integer)

On se connecte a la base de données, l’engine détermine le type par rapport au protocole de connection, ici sqlite:

# Création de l'engine de la base de données (ici SQLite)
engine = create_engine('sqlite:///:memory:')

Puis on peut interagir avec la base de données au travers de sessions

# Création d'une session pour interagir avec la base de données
Session = sessionmaker(bind=engine)
session = Session()
# Création d'un nouvel utilisateur
nouvel_utilisateur = Utilisateur(nom="John", age=30)

# Ajout de l'utilisateur à la session
session.add(nouvel_utilisateur)
Pour aller plus loin

Voir l’exemple : sql_alchemy.py

Pour aller plus loin

SqlAlchemy dans une archi projet : exemple de thread stackoverflow

Quelques pistes pour vos projets

  • Travail en local avec des bases de données locales : mysql sqlite (partage du fichier db éventuellement ou d’un db au démarrage)

  • Installation de bases de données postgresql mongodb sur les instances du SSPCLOUD. Attention, elles seront uniquement accessible du cluster et donc d’un vscode dans le cluster.

  • Installation des bases de données en local :

Mongodb : Suivre la documentation ici https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/

Postgresql: Suivre la documentation ici https://ubuntu.com/server/docs/install-and-configure-postgresql

  • Utilisation de docker (voir slide prochain cours)

Mongodb: docker run -d --name mongodb -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=motdepasseadmin -p 27017:27017 mongo

Postgresql: docker run postgres -d -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=motdepasseadmin -e POSTGRES_DB=postgres -p 5432:5432

Attendus projets

  • Il sera attendu de vous de nous fournir un script d’initialisation de base de données ou un fichier pour démarrer dans votre projet si vous implémentez de la persistence.
  • Le choix de la BDD vous incombe
  • La modélisation en base de données ne nécessite pas de diagramme pour le rendu, mais sera analysée pour les items qualité du code / fonctionnel.