Dans cet exercice, tu vas apprendre à charger des données JSON dans une base MySQL : c’est ce qu’on appelle un initial load. Concrètement, nous partirons d’un fichier films.json contenant une liste de films (titre, année, acteurs, genres, images, etc.) et nous allons écrire un script Python qui crée les tables nécessaires (migrations simples), puis importe les données de manière idempotente : c’est-à-dire qu’on pourra relancer le script plusieurs fois sans créer de doublons, tout en mettant à jour les infos si elles changent. Cet exercice te permettra de comprendre les étapes clés d’un pipeline d’import : créer un schéma relationnel, lire du JSON, insérer avec des contraintes, gérer les relations entre tables et vérifier le résultat avec des requêtes SQL.
En informatique (et en particulier avec les bases de données), une migration est une opération qui modifie la structure de la base afin de l’adapter aux besoins de l’application : création de tables, ajout de colonnes, ajout de clés, modification de types, etc.
Un initial load est la toute première opération de chargement de données depuis une source (JSON, CSV, API…) vers une base de données ou un système cible. L’objectif est de peupler la base à partir de zéro avec un jeu complet et propre, qui servira ensuite de référence pour les mises à jour ou les ajouts incrémentaux.
Dans ce contexte, l’idempotence veut dire que ton script d’import peut être relancé plusieurs fois sans créer de doublons ni abîmer la base.
Exemple concret :
external_id), il ne doit pas être inséré une deuxième fois → il est simplement mis à jour si ses infos ont changé.👉 Résultat : que tu lances l’import 1 fois ou 10 fois, la base contient le même état cohérent. C’est ça, l’idempotence.
Objectif : écrire un petit script Python qui :
films.json,Créé un schéma entités-associations d'abord, ainsi que la conversion en version "physique" pour MySql (tables, tables de jointure, PK's, FK's...).
MySQL en local (ou Docker).
Un utilisateur MySQL avec droits de création : user/password.
Paquet Python :
pip install mysql-connector-python
Fichier JSON : data/films.json (ton tableau de 25 films).
Nous allons créer 6 tables, mais de manière guidée :
film(id PK AI, external_id UNIQUE, titre, annee, realisateur, duree_min, resume)genre(id PK AI, nom UNIQUE)film_genre(film_id FK, genre_id FK, PK(film_id, genre_id))acteur(id PK AI, nom UNIQUE)film_acteur(film_id FK, acteur_id FK, role, PK(film_id, acteur_id))image(id PK AI, film_id FK, path)Idempotence :
film.external_id= l’iddu JSON (UNIQUE) → on peut faire un upsert.genre.nometacteur.nomsont UNIQUE → pas de doublons.
But : créer si n’existe pas (safe à relancer).
# migrate.py
import mysql.connector as mysql
DB = dict(host="127.0.0.1", user="root", password="password", database="cinema", auth_plugin="mysql_native_password")
DDL = [
# Charset/Collation pour bien gérer les accents
"CREATE DATABASE IF NOT EXISTS cinema CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;",
"USE cinema;",
"""
CREATE TABLE IF NOT EXISTS film (
id INT PRIMARY KEY AUTO_INCREMENT,
external_id INT NOT NULL,
titre VARCHAR(255) NOT NULL,
annee INT NOT NULL,
realisateur VARCHAR(255),
duree_min INT,
resume TEXT,
UNIQUE KEY uq_film_external_id (external_id),
INDEX idx_film_titre (titre),
INDEX idx_film_annee (annee)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""",
"""
CREATE TABLE IF NOT EXISTS genre (
id INT PRIMARY KEY AUTO_INCREMENT,
nom VARCHAR(100) NOT NULL,
UNIQUE KEY uq_genre_nom (nom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""",
"""
CREATE TABLE IF NOT EXISTS film_genre (
film_id INT NOT NULL,
genre_id INT NOT NULL,
PRIMARY KEY (film_id, genre_id),
FOREIGN KEY (film_id) REFERENCES film(id) ON DELETE CASCADE,
FOREIGN KEY (genre_id) REFERENCES genre(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""",
"""
CREATE TABLE IF NOT EXISTS acteur (
id INT PRIMARY KEY AUTO_INCREMENT,
nom VARCHAR(255) NOT NULL,
UNIQUE KEY uq_acteur_nom (nom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""",
"""
CREATE TABLE IF NOT EXISTS film_acteur (
film_id INT NOT NULL,
acteur_id INT NOT NULL,
role VARCHAR(255),
PRIMARY KEY (film_id, acteur_id),
FOREIGN KEY (film_id) REFERENCES film(id) ON DELETE CASCADE,
FOREIGN KEY (acteur_id) REFERENCES acteur(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""",
"""
CREATE TABLE IF NOT EXISTS image (
id INT PRIMARY KEY AUTO_INCREMENT,
film_id INT NOT NULL,
path VARCHAR(400) NOT NULL,
FOREIGN KEY (film_id) REFERENCES film(id) ON DELETE CASCADE,
INDEX idx_image_film (film_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
]
def main():
conn = mysql.connect(**DB)
cur = conn.cursor()
for stmt in DDL:
cur.execute(stmt)
conn.commit()
cur.close()
conn.close()
print("✅ Migrations OK (tables prêtes).")
if __name__ == "__main__":
main()
À comprendre
CREATE TABLE IF NOT EXISTS → relancer sans casser.Stratégie simple :
external_id UNIQUE (sans dupliquer).get_or_create pour genre et acteur (sur leur nom UNIQUE).# import_films.py
import json
import mysql.connector as mysql
DB = dict(host="127.0.0.1", user="root", password="password", database="cinema", auth_plugin="mysql_native_password")
def get_conn():
return mysql.connect(**DB)
def get_or_create_genre(cur, nom):
# essaie d'insérer, sinon récupère l'id existant
cur.execute("INSERT IGNORE INTO genre(nom) VALUES (%s)", (nom,))
cur.execute("SELECT id FROM genre WHERE nom=%s", (nom,))
return cur.fetchone()[0]
def get_or_create_acteur(cur, nom):
cur.execute("INSERT IGNORE INTO acteur(nom) VALUES (%s)", (nom,))
cur.execute("SELECT id FROM acteur WHERE nom=%s", (nom,))
return cur.fetchone()[0]
def upsert_film(cur, f):
"""
Retourne l'id interne du film après upsert.
ON DUPLICATE KEY UPDATE garantit l'idempotence.
"""
cur.execute("""
INSERT INTO film (external_id, titre, annee, realisateur, duree_min, resume)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
titre=VALUES(titre),
annee=VALUES(annee),
realisateur=VALUES(realisateur),
duree_min=VALUES(duree_min),
resume=VALUES(resume)
""", (f["id"], f["titre"], f["annee"], f.get("realisateur"), f.get("duree_min"), f.get("resume")))
# Récupérer l'id interne
cur.execute("SELECT id FROM film WHERE external_id=%s", (f["id"],))
return cur.fetchone()[0]
def link_film_genre(cur, film_id, genre_id):
# évite les doublons grâce à la PK(film_id, genre_id)
cur.execute("""
INSERT IGNORE INTO film_genre (film_id, genre_id) VALUES (%s, %s)
""", (film_id, genre_id))
def link_film_acteur(cur, film_id, acteur_id, role):
cur.execute("""
INSERT INTO film_acteur (film_id, acteur_id, role)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE role=VALUES(role)
""", (film_id, acteur_id, role))
def insert_images(cur, film_id, images):
# simple: on ajoute; si tu veux l'idempotence stricte, vérifie l'existence du path avant
for p in images:
cur.execute("INSERT INTO image (film_id, path) VALUES (%s, %s)", (film_id, p))
def import_json(path_json):
with open(path_json, encoding="utf-8") as f:
films = json.load(f)
conn = get_conn()
cur = conn.cursor()
try:
# petite transaction globale (ici dataset modeste)
conn.start_transaction()
for f in films:
# 1) upsert film
film_id = upsert_film(cur, f)
# 2) genres
for g in f.get("genres", []):
gid = get_or_create_genre(cur, g.strip())
link_film_genre(cur, film_id, gid)
# 3) acteurs
for a in f.get("acteurs", []):
aid = get_or_create_acteur(cur, a["nom"].strip())
role = a.get("role")
link_film_acteur(cur, film_id, aid, role)
# 4) images (option simple : on ne déduplique pas ici)
insert_images(cur, film_id, f.get("images", []))
conn.commit()
print(f"✅ Import OK ({len(films)} films).")
except Exception as e:
conn.rollback()
print("❌ Erreur, rollback :", e)
finally:
cur.close()
conn.close()
if __name__ == "__main__":
import_json("data/films.json")
Points pédagogiques clés
INSERT IGNORE + UNIQUE → get_or_create sans si/else compliqué.ON DUPLICATE KEY UPDATE → upsert propre.SELECT COUNT(*) FROM film;
SELECT id, external_id, titre, annee FROM film WHERE external_id=101;
SELECT g.nom
FROM film f
JOIN film_genre fg ON fg.film_id=f.id
JOIN genre g ON g.id=fg.genre_id
WHERE f.external_id=101;
SELECT a.nom, fa.role
FROM film f
JOIN film_acteur fa ON fa.film_id=f.id
JOIN acteur a ON a.id=fa.acteur_id
WHERE f.external_id=101;
SELECT path FROM image i
JOIN film f ON f.id=i.film_id
WHERE f.external_id=101;
Toujours mettre utf8mb4 pour gérer accents/emoji.
Normaliser strings : strip(), éviter espaces doubles.
Ne jamais mettre d’URL en dur côté Flask : stocke juste le path d’image.
Commence petit : teste d’abord avec 3 films, puis 25.
Si tu veux que les images soient aussi idempotentes, remplace insert_images par :
cur.execute("SELECT 1 FROM image WHERE film_id=%s AND path=%s", (film_id, p))
if not cur.fetchone():
cur.execute("INSERT INTO image (film_id, path) VALUES (%s, %s)", (film_id, p))
Pour une première heure de cours : 2 tables seulement.
film(id PK AI, external_id UNIQUE, titre, annee, resume)image(id PK AI, film_id FK, path)Idées : ignorer dans un premier temps genre, acteur, puis ajouter ces tables à l’exercice 2 (migration qui étend le schéma, et script d’import qui les remplit).
python migrate.py → “✅ Migrations OK”.python import_films.py → “✅ Import OK (25 films)”.python import_films.py → aucun doublon, mais les valeurs mises à jour si tu modifies le JSON.--dry-run qui affiche les actions sans écrire.get_or_create_*.WHERE annee=…, LIKE titre).Si tu veux, je te fournis une version monofichier qui fait migrations + import en un seul python app.py (avec un petit menu texte).