LYCÉE → PRÉPA · L08

Module L08 · Partie F · Coder comme un professionnel

Données persistantes et réseau : SQL, HTTP, API.

Un robot produit des milliers de mesures par minute ; un projet d’IA en manipule des millions. Les stocker dans des listes ne tient pas. Ce module présente le modèle relationnel et SQL (exécuté dans la page avec SQLite), puis le web : HTTP, JSON, une API REST, et comment un tableau de bord parle à un robot.

Durée : 2 séances · Prérequis : L01, dictionnaires, fichiers. Objectifs : concevoir un schéma relationnel, écrire des requêtes SQL (SELECT, JOIN, GROUP BY, sous-requêtes, index), comprendre transactions et injections, HTTP et JSON, écrire un serveur et un client minimaux.

Ce que vous saurez faire à la fin
  • Modéliser des données en tables avec clés primaires et étrangères, en évitant la redondance.
  • Écrire et optimiser des requêtes SQL sur des centaines de milliers de lignes.
  • Exposer les données d’un robot via une API HTTP/JSON et les consommer depuis Python ou un navigateur.

Références : programme NSI terminale (bases de données), freeCodeCamp « Relational Database », Designing Data-Intensive Applications (Kleppmann) chapitres 1-3, MDN Web Docs.

Fiche de cours · Définitions

Bases de données et web : définitions

Définition (modèle relationnel). Une relation (table) est un ensemble de tuples (lignes) sur un schéma (attributs typés). Une clé primaire identifie chaque ligne de façon unique ; une clé étrangère référence la clé primaire d’une autre table (intégrité référentielle). Le résultat d’une requête est encore une relation.
Définition (algèbre relationnelle). Opérateurs : sélection σcondition (WHERE), projection πattributs (SELECT), produit cartésien ×, jointure ⋈ (produit + sélection sur l’égalité des clés), union, différence, renommage. SQL en est une syntaxe, augmentée d’agrégats (COUNT, SUM, AVG, GROUP BY) et de tri.
Définition (formes normales). 1FN : valeurs atomiques (pas de liste dans une case). 2FN : tout attribut non-clé dépend de toute la clé. 3FN : aucun attribut non-clé ne dépend d’un autre attribut non-clé (pas de dépendance transitive). Normaliser élimine les redondances et les anomalies de mise à jour.
Définition (transaction, ACID). Suite d’opérations exécutée comme un tout : Atomicité (tout ou rien), Cohérence (contraintes respectées), Isolation (transactions concurrentes invisibles l’une à l’autre), Durabilité (validé = persistant).
Définition (HTTP, REST, JSON). HTTP : protocole requête/réponse sans état ; méthode (GET lit, POST crée, PUT remplace, DELETE supprime), URL, en-têtes, corps, code de statut (2xx succès, 4xx erreur client, 5xx erreur serveur). REST : exposer des ressources par URL et méthodes. JSON : format texte d’échange (objets, tableaux, nombres, chaînes, booléens, null).
Définition (index). Structure auxiliaire (B-arbre) sur une colonne permettant de trouver les lignes en O(log n) au lieu d’un balayage O(n), au prix d’un coût à chaque écriture.

Fiche de cours · Formules

Formules et équivalences à connaître

AlgèbreSQLCoût naïf
σc(R)SELECT * FROM R WHERE cO(|R|), O(log |R|) avec index
πa,b(R)SELECT DISTINCT a, b FROM RO(|R|) (+ tri pour DISTINCT)
R ⋈R.k = S.k SFROM R JOIN S ON R.k = S.kO(|R|·|S|) boucles imbriquées ; O(|R| + |S|) par hachage
γg, f(a)(R)SELECT g, f(a) FROM R GROUP BY gO(|R|) par hachage
R − S… WHERE k NOT IN (SELECT k FROM S)O(|R| + |S|) par hachage
|R ⋈ S| ≤ |R| · |S| ; si k est clé de S : |R ⋈k S| ≤ |R| une jointure sur clé étrangère → clé primaire ne multiplie pas les lignes
Ordre d’évaluation d’un SELECT : FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT d’où : WHERE ne connaît pas les agrégats (HAVING oui), ORDER BY connaît les alias du SELECT

Codes HTTP à connaître : 200 OK, 201 Created, 204 No Content, 301/302 redirection, 304 Not Modified, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, 500 Internal Server Error, 503 Service Unavailable.

Fiche de cours · Théorèmes et démonstrations

Démonstrations à savoir refaire

Théorème 1 (anomalies sans normalisation). Une table Commande(num, client, adresse_client, produit) qui stocke l’adresse du client dans chaque commande présente trois anomalies : mise à jour (changer une adresse exige de modifier toutes ses commandes, avec risque d’incohérence), insertion (impossible d’enregistrer un client sans commande), suppression (supprimer la dernière commande efface l’adresse).
L’attribut adresse_client dépend de client, qui n’est pas la clé (num) : dépendance transitive num → client → adresse_client, violation de la 3FN. Chaque anomalie découle du fait qu’une information (l’adresse) est répétée autant de fois qu’il y a de commandes, ou n’existe que s’il y a une commande. Décomposer en Client(client, adresse) et Commande(num, client, produit) — jointure sans perte car client est clé de Client — supprime les trois anomalies : une seule ligne porte l’adresse.
Théorème 2 (jointure par hachage en temps linéaire). R ⋈k S se calcule en O(|R| + |S| + |résultat|) en moyenne.
Construire une table de hachage de S indexée par k : O(|S|) insertions O(1) moyen. Parcourir R : pour chaque ligne, chercher sa clé dans la table (O(1) moyen) et émettre les couples appariés. Le total est O(|R| + |S|) plus le temps d’écrire le résultat. Les boucles imbriquées coûtent O(|R|·|S|) : pour deux tables de 10⁵ lignes, 10¹⁰ contre 2·10⁵. C’est ce que fait l’optimiseur de SQLite/PostgreSQL ; un index B-arbre sur S.k donne O(|R| log |S|), utile quand S est déjà indexée.
Théorème 3 (injection SQL). Construire une requête par concaténation de chaînes avec une entrée utilisateur permet à l’utilisateur d’exécuter des requêtes arbitraires.
Soit la requête "SELECT * FROM users WHERE nom = '" + nom + "'". Avec nom = ' OR '1'='1, elle devient SELECT * FROM users WHERE nom = '' OR '1'='1', vraie pour toute ligne. Avec nom = '; DROP TABLE users; --, deux requêtes sont exécutées. Le texte de l’utilisateur est interprété comme du code parce que rien ne sépare données et code. Les requêtes paramétrées (cur.execute("… WHERE nom = ?", (nom,))) transmettent la donnée séparément : elle n’est jamais analysée comme SQL, quel que soit son contenu. C’est la seule protection correcte (l’échappement manuel est fragile).

Fiche de cours · Méthodes

Méthodes et pièges

Méthode — concevoir un schéma. (1) Lister les entités (noms) et leurs attributs. (2) Lister les associations et leur cardinalité : 1–N → clé étrangère côté N ; N–N → table d’association (deux clés étrangères, clé primaire composite). (3) Vérifier la 3FN : chaque attribut dépend de la clé, toute la clé, rien que la clé. (4) Contraintes : NOT NULL, UNIQUE, CHECK, FOREIGN KEY. (5) Index sur les colonnes filtrées et jointes fréquemment.
Méthode — écrire une requête complexe. Partir du résultat voulu (colonnes), identifier les tables nécessaires, écrire les jointures sur les clés, filtrer (WHERE), regrouper (GROUP BY + agrégats), filtrer les groupes (HAVING), trier. Tester sur un petit jeu où le résultat est connu. EXPLAIN QUERY PLAN pour voir si un index est utilisé.
Méthode — concevoir une API REST. Ressources au pluriel (/robots, /robots/42/missions) ; GET sans effet de bord ; codes de statut corrects ; erreurs en JSON avec message ; pagination (?page=2&size=50) ; validation des entrées ; authentification par jeton ; journaliser chaque requête.

Pièges : SELECT * dans du code (fragile aux changements de schéma) ; oublier la condition de jointure (produit cartésien) ; NULL n’est égal à rien (= NULL est toujours faux : IS NULL) ; agrégat sans GROUP BY sur des colonnes non agrégées ; stocker des mots de passe en clair (hacher avec sel, L22) ; GET qui modifie des données.

Fiche de cours · Exercices corrigés

Exercices corrigés

Exercice 1. Schéma : Robot(id, nom), Mission(id, titre, date), Participation(robot_id, mission_id, role). Écrire : (a) les robots n’ayant participé à aucune mission ; (b) pour chaque mission, le nombre de robots ; (c) les missions auxquelles ont participé tous les robots.
Correction.
-- (a) différence : robots sans participation
SELECT r.nom FROM Robot r LEFT JOIN Participation p ON p.robot_id = r.id WHERE p.robot_id IS NULL;
-- (b) agrégat par mission (LEFT JOIN pour garder les missions à 0 robot)
SELECT m.titre, COUNT(p.robot_id) AS n FROM Mission m LEFT JOIN Participation p ON p.mission_id = m.id GROUP BY m.id;
-- (c) division relationnelle : missions dont le nombre de robots distincts = nombre total de robots
SELECT m.titre FROM Mission m JOIN Participation p ON p.mission_id = m.id
GROUP BY m.id HAVING COUNT(DISTINCT p.robot_id) = (SELECT COUNT(*) FROM Robot);
(c) est la « division » : il n’y a pas d’opérateur SQL direct, on compte.
Exercice 2. La table Mesure(robot, capteur, unite, valeur, t) contient 10⁷ lignes ; la requête « valeurs du capteur X du robot Y entre t₁ et t₂ » prend 4 s. Que faire ? Pourquoi unite pose un problème de normalisation ?
Correction. Sans index, la requête balaie 10⁷ lignes. Un index composite CREATE INDEX ON Mesure(robot, capteur, t) permet d’aller directement à la plage (B-arbre : O(log n) + taille du résultat) : quelques millisecondes. L’ordre des colonnes dans l’index doit suivre les égalités (robot, capteur) puis l’intervalle (t). unite dépend de capteur, pas de la clé complète (robot, capteur, t) : dépendance partielle, violation de la 2FN, 10⁷ répétitions de « m/s ». Déplacer dans Capteur(nom, unite).
Exercice 3. Un serveur expose GET /robots/{id}/position et POST /robots/{id}/commande avec un corps JSON {"v": 0.3, "w": 0.1}. Donner les réponses attendues (codes, corps) pour : robot inexistant, corps malformé, commande acceptée, robot en panne.
Correction. Robot inexistant : 404 {"erreur": "robot 7 inconnu"}. JSON invalide ou champ manquant/hors plage (v = 12) : 400 {"erreur": "v doit être dans [-1, 1]"} — valider avant d’agir. Commande acceptée : 202 Accepted (si exécution asynchrone) ou 200 avec l’état résultant {"v": 0.3, "w": 0.1, "t": …}. Robot en panne : 503 Service Unavailable avec Retry-After, ou 409 Conflict si l’état du robot interdit la commande. GET position : 200 {"x": …, "y": …, "theta": …, "t": …}, jamais de modification d’état. Chaque requête est journalisée avec son code pour les métriques.

01 / Relationnel

Tables, clés, relations

Pourquoi trois tables et pas une ?

Une seule table « mesure » avec les colonnes robot_nom, robot_modele, capteur_type… répéterait « R2, explorateur, ultrason, cm » 200 fois : gaspillage, et si le robot est renommé, il faut modifier 200 lignes (risque d’incohérence). La normalisation : chaque fait est stocké une fois, les tables se référencent par des clés. La clé primaire identifie une ligne ; la clé étrangère pointe vers la clé primaire d’une autre table. SQLite est une vraie base SQL dans un fichier, présente dans chaque téléphone et navigateur ; PostgreSQL et MySQL ajoutent le réseau et la concurrence.

01 / Relationnel

SELECT : filtrer, trier, agréger

L’ordre logique d’un SELECT : FROM (tables) → WHERE (filtrer les lignes) → GROUP BY (regrouper) → HAVING (filtrer les groupes) → SELECT (colonnes calculées) → ORDER BY → LIMIT. Écrire dans cet ordre mental évite 90 % des erreurs.

01 / Relationnel

JOIN : recoller les tables

Les jointures en une image

JOIN (interne) : seulement les lignes qui ont une correspondance des deux côtés. LEFT JOIN : toutes les lignes de gauche, avec NULL à droite si rien ne correspond. Une jointure est mathématiquement un produit cartésien filtré ; le moteur SQL choisit l’algorithme (boucles imbriquées, hachage, tri-fusion — les mêmes que dans L03-L04) selon les index et les statistiques. Ce planificateur de requêtes est l’un des logiciels les plus sophistiqués qui existent : vous dites quoi, il décide comment. C’est le paradigme déclaratif.

01 / Relationnel

Index, transactions, EXPLAIN : ce qui fait la différence à l’échelle

ACID et index

Un index est un arbre B (cousin de l’AVL du module L03) sur une ou plusieurs colonnes : la recherche passe de O(n) (« SCAN ») à O(log n) (« SEARCH USING INDEX »). Il coûte de l’espace et ralentit un peu les insertions : on indexe les colonnes des WHERE et JOIN fréquents, pas tout. Une transaction garantit ACID : Atomicité (tout ou rien), Cohérence (les contraintes tiennent), Isolation (les transactions concurrentes ne se voient pas à moitié), Durabilité (une fois validé, c’est sur le disque même si le courant coupe). Un journal de robot écrit par transactions de 100 mesures survit à un arrêt brutal.

02 / Web

HTTP : une requête, une réponse, du texte

GET /api/robots/1/mesures?depuis=120&limite=3 HTTP/1.1
Host: robot.local
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 142

{"robot": "R2", "mesures": [{"t": 120.0, "valeur": 61.2}, {"t": 120.1, "valeur": 60.8}, {"t": 120.2, "valeur": 59.9}]}
Méthodes, codes, en-têtes
MéthodeSensExemple
GETLire (sans effet)GET /api/mesures
POSTCréer / envoyerPOST /api/commandes avec un corps JSON
PUT / PATCHRemplacer / modifierPATCH /api/robots/1
DELETESupprimerDELETE /api/mesures/42

Codes : 200 OK, 201 créé, 400 requête invalide, 401/403 non autorisé, 404 introuvable, 500 erreur serveur. HTTPS = HTTP chiffré par TLS : sans lui, tout ce qui transite (mots de passe compris) est lisible par le réseau. Une API REST organise les URL par ressources (noms) et les actions par méthodes (verbes).

02 / Web

Un serveur d’API pour le robot (Flask), et son client

# serveur.py — sur le Raspberry Pi du robot :  pip install flask
from flask import Flask, jsonify, request
import sqlite3

app = Flask(__name__)
def db(): return sqlite3.connect("robot.db")

@app.get("/api/mesures")
def mesures():
    capteur = request.args.get("capteur", type=int)
    limite = request.args.get("limite", 100, type=int)
    with db() as c:
        lignes = c.execute("SELECT t, valeur FROM mesure WHERE capteur_id = ? ORDER BY t DESC LIMIT ?",
                           (capteur, limite)).fetchall()
    return jsonify([{"t": t, "valeur": v} for t, v in lignes])

@app.post("/api/commandes")
def commande():
    corps = request.get_json()
    if corps.get("action") not in ("avancer", "stop", "tourner"):
        return jsonify({"erreur": "action inconnue"}), 400
    file_commandes.put(corps)          # traitée par le thread de contrôle (séance 20)
    return jsonify({"ok": True}), 201

app.run(host="0.0.0.0", port=8080)
# client.py — sur le PC :  pip install requests
import requests
r = requests.get("http://robot.local:8080/api/mesures", params={"capteur": 1, "limite": 5})
print(r.status_code, r.json())
r = requests.post("http://robot.local:8080/api/commandes", json={"action": "avancer", "vitesse": 0.4})
print(r.status_code, r.json())
<!-- tableau de bord : une page HTML qui interroge l'API toutes les secondes -->
<script>
setInterval(async () => {
  const r = await fetch("/api/mesures?capteur=1&limite=1");
  const [m] = await r.json();
  document.querySelector("#distance").textContent = m.valeur.toFixed(1) + " cm";
}, 1000);
</script>

La séance 26 faisait cela par port série ; ici le robot est un serveur web : n’importe quel appareil du réseau (téléphone, autre robot) peut lire ses données et lui envoyer des ordres. FastAPI est l’alternative moderne à Flask (types + documentation automatique).

02 / Web

Simuler client et serveur dans la page

Le décorateur @route est exactement ce que fait Flask : un registre (patron Fabrique, L02) qui associe (méthode, chemin) → fonction. Le vrai serveur ajoute la socket réseau, l’analyse du texte HTTP et la concurrence.

03 / Sécurité

Les erreurs qui coûtent cher

FailleCauseParade
Injection SQLConcaténer l’entrée utilisateur dans la requêteRequêtes paramétrées (?)
XSSAfficher du texte utilisateur comme HTMLÉchapper (html.escape) ; les frameworks le font par défaut
Mots de passe en clairStocker le mot de passeStocker un hachage lent et salé (bcrypt, argon2) ; jamais SHA-1/MD5
Secrets dans GitClé d’API dans le codeVariables d’environnement, fichier .env ignoré
Robot sur Internet sans authentification« C’est juste un test »Réseau local, jeton d’API, HTTPS
Débordement de tampon (C)Lire une trame réseau dans un tableau fixe sans vérifierToujours borner (strncpy, vérifier les longueurs) — module L18

Cours

Cours 1 — Modéliser : entités, relations, cardinalités, formes normales

Méthode. 1) Lister les entités (noms de l’énoncé : robot, capteur, mesure, mission) et leurs attributs ; 2) les relations avec leur cardinalité : un robot a plusieurs capteurs (1–N : clé étrangère côté N), un robot participe à plusieurs missions et une mission implique plusieurs robots (N–N : table de liaison participation(robot_id, mission_id, …)) ; 3) choisir les clés primaires (entier auto-incrémenté, ou clé naturelle unique) ; 4) normaliser.

Forme normaleRègleViolation typiqueCorrection
1NFChaque cellule contient une valeur atomiqueColonne capteurs = "ultrason,ir,imu"Table capteur, une ligne par capteur
2NFTout attribut dépend de toute la cléDans participation(robot_id, mission_id, nom_robot)nom_robot va dans robot
3NFAucun attribut ne dépend d’un attribut non-clémesure(capteur_id, unite) alors que l’unité dépend du type de capteurunite dans capteur (ou type_capteur)

Pourquoi normaliser : éviter les anomalies de mise à jour (changer un nom en 200 endroits), d’insertion (impossible d’ajouter un capteur sans mesure) et de suppression (supprimer la dernière mesure fait disparaître le capteur). Quand dénormaliser : pour la lecture massive (entrepôts de données, tableaux de bord) on recopie volontairement, en acceptant la redondance contrôlée.

Cours

Cours 2 — Algèbre relationnelle : les cinq opérations derrière SQL

OpérationNotationSQLRésultat
Sélectionσcondition(R)WHERELes lignes qui vérifient la condition
Projectionπcolonnes(R)SELECT a, b (+ DISTINCT pour la vraie projection ensembliste)Certaines colonnes
Produit cartésienR × SFROM R, S ou CROSS JOINToutes les paires de lignes
JointureR ⋈cond S = σcond(R × S)JOIN … ONPaires compatibles
Union / différence / intersection∪, −, ∩UNION, EXCEPT, INTERSECTSur des tables de même schéma
Agrégationγgroupe ; f(R)GROUP BY + COUNT/SUM/AVG/MIN/MAXUne ligne par groupe

Le sens d’une requête, c’est sa traduction. « Les robots qui n’ont participé à aucune mission » = πnom(robot) − πnom(robot ⋈ participation). En SQL : SELECT nom FROM robot EXCEPT SELECT r.nom FROM robot r JOIN participation p ON p.robot_id = r.id, ou avec NOT EXISTS, ou LEFT JOIN … WHERE p.robot_id IS NULL : trois écritures, un seul sens. Savoir passer de l’une à l’autre est ce qu’on attend en NSI et en entretien.

Cours

Cours 3 — HTTP en détail : requête, réponse, état, cache, authentification

POST /api/commandes HTTP/1.1
Host: robot.local:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...          ← jeton : prouve qui envoie
Content-Length: 34

{"action": "avancer", "vitesse": 0.4}

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/commandes/57                   ← où est la ressource créée
Cache-Control: no-store                       ← ne jamais mettre en cache une commande

{"id": 57, "ok": true}
NotionCe qu’il faut savoir
Sans étatChaque requête est indépendante ; le serveur ne se souvient de rien. L’« état » (session) est porté par un cookie ou un jeton envoyé à chaque fois.
IdempotenceGET, PUT, DELETE peuvent être répétés sans effet supplémentaire ; POST non. Un client qui rejoue un POST après un timeout peut créer un doublon : d’où les clés d’idempotence.
Codes2xx succès, 3xx redirection, 4xx erreur du client (400 mal formé, 401 non authentifié, 403 interdit, 404 introuvable, 429 trop de requêtes), 5xx erreur du serveur.
CacheCache-Control: max-age=60 : le navigateur ne redemande pas pendant 60 s. C’est pourquoi ce site ajoute ?v=… aux fichiers modifiés.
CORSUn navigateur refuse qu’une page de A appelle l’API de B sauf si B l’autorise (Access-Control-Allow-Origin). Ce n’est pas une protection du serveur, c’est une protection de l’utilisateur.
WebSocketConnexion bidirectionnelle persistante pour le temps réel (télémétrie, chat) ; SSE pour du serveur → client seulement.

TP guidé

TP — Base de données, API et tableau de bord d’une flotte (sur PC, 3 h)

Exercices

Exercices auto-corrigés — SQL

Exercice 1 — Écrire les requêtes

Sur la base c2 (robot, mission, participation) ci-dessus, complétez les chaînes SQL. La cellule exécute et compare.

Correction
R1 = "SELECT r.nom FROM robot r JOIN participation p ON p.robot_id = r.id GROUP BY r.id HAVING COUNT(*) >= 2"
R2 = "SELECT m.nom, COUNT(p.robot_id) FROM mission m LEFT JOIN participation p ON p.mission_id = m.id GROUP BY m.id ORDER BY m.nom"
R3 = "SELECT r.nom, m.nom FROM participation p JOIN robot r ON r.id = p.robot_id JOIN mission m ON m.id = p.mission_id WHERE p.role IN ('explorateur', 'vedette') ORDER BY r.nom, m.nom"

Dans R2, COUNT(p.robot_id) (et non COUNT(*)) compte 0 pour la mission sans participant : COUNT(colonne) ignore les NULL.

Exercice 2 — Transactions et contraintes

Écrivez transferer_role(cur2, robot_a, robot_b, mission_id) qui donne à B le rôle de A sur la mission et retire A, de façon atomique (tout ou rien) : si B n’existe pas ou si A n’est pas sur la mission, rien ne change et une exception est levée.

Correction
def transferer_role(cur2, a, b, m):
    cur2.execute("SAVEPOINT t")
    try:
        role = cur2.execute("SELECT role FROM participation WHERE robot_id = ? AND mission_id = ?", (a, m)).fetchone()
        if role is None or cur2.execute("SELECT 1 FROM robot WHERE id = ?", (b,)).fetchone() is None:
            raise ValueError("transfert impossible")
        cur2.execute("DELETE FROM participation WHERE robot_id = ? AND mission_id = ?", (a, m))
        cur2.execute("INSERT INTO participation VALUES (?, ?, ?)", (b, m, role[0]))
        cur2.execute("RELEASE t")
    except Exception:
        cur2.execute("ROLLBACK TO t"); cur2.execute("RELEASE t"); raise

Exercices

Exercices auto-corrigés — HTTP et JSON

Exercice 3 — Analyser une requête HTTP brute

Écrivez analyser(texte) qui découpe une requête HTTP (ligne de requête, en-têtes, corps) en dictionnaire : methode, chemin, params (query string décodée), entetes (clés en minuscules), corps (JSON décodé si content-type est application/json, sinon texte).

Correction
def analyser(texte):
    tete, _, corps = texte.partition("\r\n\r\n")
    lignes = tete.split("\r\n"); methode, cible, _ = lignes[0].split(" ")
    u = urlparse(cible); entetes = {}
    for l in lignes[1:]:
        k, _, v = l.partition(":"); entetes[k.strip().lower()] = v.strip()
    if entetes.get("content-type", "").startswith("application/json") and corps: corps = json.loads(corps)
    return {"methode": methode, "chemin": u.path, "params": parse_qs(u.query), "entetes": entetes, "corps": corps}

Exercice 4 — Valider un JSON de commande

valider(commande) renvoie une liste d’erreurs (vide si valide) : action obligatoire ∈ {avancer, stop, tourner} ; vitesse nombre dans [−1, 1] obligatoire pour avancer ; angle nombre dans [−180, 180] obligatoire pour tourner ; toute clé inconnue est une erreur. Message : "champ: raison".

Correction
def valider(c):
    err = []; connues = {"action", "vitesse", "angle"}
    err += [f"{k}: inconnu" for k in c if k not in connues]
    a = c.get("action")
    if a not in ("avancer", "stop", "tourner"): err.append("action: obligatoire, parmi avancer/stop/tourner")
    def nombre(champ, lo, hi):
        if champ not in c: err.append(f"{champ}: obligatoire")
        elif not isinstance(c[champ], (int, float)) or not lo <= c[champ] <= hi: err.append(f"{champ}: nombre entre {lo} et {hi} attendu")
    if a == "avancer": nombre("vitesse", -1, 1)
    if a == "tourner": nombre("angle", -180, 180)
    return err

04 / Défis

Défi ★ — Le journal de bord en SQL

Consigne

Sur la base du module : 1) la mesure maximale de chaque capteur avec son instant ; 2) pour chaque robot, le nombre de mesures d’ultrason inférieures à 20 cm (obstacles proches) ; 3) les 3 secondes (entières) où la température moyenne était la plus haute ; 4) un rapport « robot, type, nb, moyenne, écart-type » (l’écart-type se calcule avec AVG(valeur*valeur) − AVG(valeur)²).

Correction
q("""SELECT capteur_id, t, valeur FROM mesure m
     WHERE valeur = (SELECT MAX(valeur) FROM mesure WHERE capteur_id = m.capteur_id)""")
q("""SELECT r.nom, COUNT(*) FROM mesure m JOIN capteur c ON m.capteur_id = c.id JOIN robot r ON c.robot_id = r.id
     WHERE c.type = 'ultrason' AND m.valeur < 20 GROUP BY r.nom""")
q("""SELECT CAST(t AS INTEGER) s, ROUND(AVG(valeur), 2) FROM mesure m JOIN capteur c ON m.capteur_id = c.id
     WHERE c.type = 'temperature' GROUP BY s ORDER BY 2 DESC LIMIT 3""")
q("""SELECT r.nom, c.type, COUNT(*), ROUND(AVG(valeur), 2),
            ROUND(SQRT(AVG(valeur * valeur) - AVG(valeur) * AVG(valeur)), 2)
     FROM mesure m JOIN capteur c ON m.capteur_id = c.id JOIN robot r ON c.robot_id = r.id
     GROUP BY r.nom, c.type""")

Si SQRT n’est pas disponible dans votre SQLite, calculez la variance en SQL et la racine en Python. La sous-requête corrélée de la question 1 est réévaluée par ligne : sur de gros volumes, préférez une jointure avec un GROUP BY.

04 / Défis

Défi ★★ — Concevoir le schéma d’une compétition de robots

Consigne

Équipes, robots (une équipe a plusieurs robots), épreuves (nom, date), participations (un robot dans une épreuve : temps, score, rang) et incidents (participation, instant, description). 1) Écrivez le schéma avec clés, contraintes NOT NULL/UNIQUE/CHECK (score ≥ 0). 2) Insérez des données de test (au moins 4 équipes, 6 robots, 3 épreuves). 3) Requêtes : classement général par équipe (somme des meilleurs scores par épreuve), robot avec le plus d’incidents, épreuve la plus disputée (écart-type des scores minimal). 4) Quel index accélère « les participations d’un robot » ? Vérifiez avec EXPLAIN.

Piste
CREATE TABLE participation (
  id INTEGER PRIMARY KEY,
  robot_id INTEGER NOT NULL REFERENCES robot(id),
  epreuve_id INTEGER NOT NULL REFERENCES epreuve(id),
  score REAL NOT NULL CHECK (score >= 0),
  temps REAL,
  UNIQUE (robot_id, epreuve_id)          -- un robot participe une fois par épreuve
);
CREATE INDEX idx_part_robot ON participation (robot_id);

Le classement « meilleur score par épreuve puis somme » demande une sous-requête groupée (meilleur score par (équipe, épreuve)) puis un GROUP BY équipe : deux niveaux d’agrégation, cas classique d’examen NSI.

04 / Défis

Défi ★★★ — Esprit prépa : un moteur de requêtes minimal

Consigne

Implémentez en Python pur une mini-algèbre relationnelle sur des listes de dictionnaires : selection(table, predicat), projection(table, colonnes), jointure(a, b, cle_a, cle_b) (version boucles imbriquées O(n·m) puis version par hachage O(n + m)), groupement(table, cle, agregats). Reproduisez la requête « robot, type, nb, moyenne » du module et vérifiez que le résultat est identique à SQLite. Mesurez les deux jointures sur 20 000 × 20 000 lignes. Question : pourquoi les bases de données choisissent-elles parfois quand même la boucle imbriquée ?

Correction (extrait)
def selection(t, p): return [l for l in t if p(l)]
def projection(t, cols): return [{c: l[c] for c in cols} for l in t]
def jointure_naive(a, b, ka, kb):
    return [{**x, **{f"b.{k}": v for k, v in y.items()}} for x in a for y in b if x[ka] == y[kb]]
def jointure_hachage(a, b, ka, kb):
    index = {}
    for y in b: index.setdefault(y[kb], []).append(y)
    return [{**x, **{f"b.{k}": v for k, v in y.items()}} for x in a for y in index.get(x[ka], [])]
def groupement(t, cle, agregats):
    groupes = {}
    for l in t: groupes.setdefault(l[cle], []).append(l)
    return [{cle: k, **{nom: f(g) for nom, f in agregats.items()}} for k, g in groupes.items()]

import time, random
A = [dict(id=i, x=random.random()) for i in range(20_000)]
B = [dict(a_id=random.randrange(20_000), y=random.random()) for _ in range(20_000)]
t0 = time.perf_counter(); n1 = len(jointure_naive(A, B, "id", "a_id")); t1 = time.perf_counter()
n2 = len(jointure_hachage(A, B, "id", "a_id")); t2 = time.perf_counter()
print(n1 == n2, f"naïve {t1 - t0:.1f} s, hachage {t2 - t1:.2f} s")

La jointure par hachage exige de tenir une table en mémoire ; si elle ne tient pas, ou si un index existe déjà sur la clé, ou si les tables sont minuscules, la boucle imbriquée (éventuellement avec index) gagne. Le planificateur choisit sur des estimations de taille — et se trompe parfois, d’où EXPLAIN.

05 / Vérification

Comment empêcher une injection SQL ?

Deux questions supplémentaires

1. Pourquoi une transaction accélère-t-elle 100× les insertions ? Sans elle, chaque INSERT est validé sur le disque (fsync) individuellement ; avec, une seule écriture durable à la fin.

2. Différence entre JOIN et LEFT JOIN ? Le LEFT JOIN garde les lignes de gauche sans correspondance (avec NULL), le JOIN les élimine.

Référence

Les mots à retenir

MotDéfinition
Clé primaire / étrangèreIdentifiant unique d’une ligne / référence vers une autre table.
NormalisationStocker chaque fait une seule fois.
JOINCombiner des tables par une condition.
GROUP BY / HAVINGAgréger par groupes / filtrer les groupes.
IndexArbre B sur une colonne : recherche O(log n).
Transaction / ACIDEnsemble d’opérations tout-ou-rien, durable.
InjectionEntrée interprétée comme du code ; parade : paramètres.
HTTPProtocole requête/réponse du web ; méthodes GET/POST…
JSONFormat texte d’échange de données structurées.
API RESTInterface HTTP organisée en ressources.
Hachage saléStockage sûr des mots de passe.

Pour continuer

Fin de la partie F : vous êtes un développeur

Partie G : les mathématiques de l’informatique. Module suivant : logique et preuves de programmes — invariants, terminaison, correction : prouver qu’un algorithme est juste, pas seulement le tester.

À faire chez soi

← L07SommaireL09 : Logique et preuves →