← HUB ✦ Holberton School — Python ✦

PYTHON ORM
MySQLdb & SQLAlchemy

Finis les requêtes SQL à la main — parle Python, laisse l'ORM traduire.

QU'EST-CE QUE C'EST ?
🎯 ORM — OBJECT RELATIONAL MAPPER

Un ORM est une couche d'abstraction entre ton code Python et ta base de données. Tu manipules des objets Python — l'ORM les traduit en SQL tout seul.

Ce projet couvre deux approches : MySQLdb (bas niveau, SQL direct) et SQLAlchemy (ORM complet, zéro SQL).

🕹️ ANALOGIE GAMING

MySQLdb c'est comme jouer sans HUD : tu gères tout manuellement, tu vois les requêtes brutes. SQLAlchemy c'est l'interface du jeu : tu cliques sur "attaquer un ennemi", le jeu traduit en mécanique interne. Même résultat, niveau d'abstraction très différent.

Sans ORM tu écris du SQL. Avec ORM tu écris du Python. Ton code devient indépendant du type de base de données.
CONCEPTS CLÉS
🔌 CONNEXION MYSQLDB

MySQLdb ouvre une connexion directe au serveur MySQL. Tu obtiens un cursor pour exécuter des requêtes SQL sous forme de chaînes de caractères.

C'est du SQL classique en Python — utile pour comprendre les bases avant d'utiliser un ORM.
⚙️ ENGINE SQLALCHEMY

L'Engine est le point d'entrée de SQLAlchemy. Il contient les infos de connexion (hôte, port, user, db) et gère le pool de connexions.

Crée-le une seule fois avec create_engine() — c'est la porte vers ta base de données.
📦 SESSION

La Session est ton espace de travail. Elle suit les objets que tu crées, modifies ou supprimes, puis les synchronise avec la base via commit().

Pense à la Session comme un panier d'achat : tu ajoutes des objets, et à la caisse (commit) tout est enregistré.
🗺️ BASE ET MODÈLE

Base est la classe parente de tous tes modèles. Chaque modèle est une classe Python qui mappe une table SQL — chaque attribut de classe = une colonne.

Un modèle = une table. Une instance = une ligne. C'est le cœur de l'ORM.
🔍 QUERY API

Au lieu d'écrire SELECT * FROM, tu utilises session.query(Model). Tu peux chaîner des filtres, tris et limites directement en Python.

SQLAlchemy traduit tes appels Python en SQL optimisé — tu n'as plus à connaître la syntaxe SQL par cœur.
🔗 POOL DE CONNEXIONS

L'option pool_pre_ping=True teste la connexion avant chaque utilisation. Indispensable pour éviter les erreurs "connexion perdue" sur un serveur idle.

Sans ça, si MySQL ferme une connexion inactive, ton code crashe. Toujours activer cette option en production.
MYSQLDB — CONNEXION DIRECTE
🔌 PATTERN COMPLET

Structure standard d'un script MySQLdb — connexion, curseur, requête, fermeture.

import MySQLdb
import sys

if __name__ == "__main__":
    # Récupère les arguments : user, password, db_name
    username = sys.argv[1]
    password = sys.argv[2]
    db_name  = sys.argv[3]

    # Ouvre la connexion au serveur MySQL local
    conn = MySQLdb.connect(
        host="localhost",
        port=3306,
        user=username,
        passwd=password,
        db=db_name,
        charset="utf8"
    )

    # Crée un curseur pour exécuter des requêtes
    cur = conn.cursor()

    # Exécute la requête SQL (ici en chaîne de caractères)
    cur.execute("SELECT * FROM states ORDER BY id ASC")

    # Récupère toutes les lignes sous forme de tuples
    rows = cur.fetchall()
    for row in rows:
        print(row)

    # Ferme curseur ET connexion — toujours dans cet ordre
    cur.close()
    conn.close()
Ne jamais concaténer des variables dans la requête SQL — risque d'injection SQL. Utiliser les paramètres : cur.execute("SELECT * FROM states WHERE name = %s", (name,))
MySQLdb retourne des tuples : (1, 'California'). Pour avoir des dicts, utiliser MySQLdb.cursors.DictCursor.
SQLALCHEMY — DÉFINIR UN MODÈLE
🗺️ MAPPER UNE CLASSE SUR UNE TABLE

Chaque modèle hérite de Base et déclare ses colonnes comme attributs de classe.

#!/usr/bin/python3
"""Module définissant le modèle State pour SQLAlchemy."""
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

# Base est la classe parente de tous les modèles
Base = declarative_base()


class State(Base):
    """Modèle représentant la table 'states' en base de données."""

    # Nom de la table en base
    __tablename__ = 'states'

    # Colonnes : id auto-incrémenté, clé primaire
    id = Column(Integer, primary_key=True, nullable=False)

    # name : chaîne de 128 chars, obligatoire
    name = Column(String(128), nullable=False)
Un attribut de classe Column(...) définit une colonne. L'ORM s'occupe de la correspondance — tu n'écris jamais le CREATE TABLE.
⚡ ENGINE + SESSION — REQUÊTER SANS SQL
#!/usr/bin/python3
"""Script listant tous les states via SQLAlchemy ORM."""
import sys
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from model_state import Base, State

if __name__ == "__main__":
    # Crée le moteur de connexion — format : dialect+driver://user:pass@host/db
    engine = create_engine(
        'mysql+mysqldb://{}:{}@localhost/{}'.format(
            sys.argv[1], sys.argv[2], sys.argv[3]
        ),
        pool_pre_ping=True  # teste la connexion avant usage
    )

    # Crée les tables si elles n'existent pas encore
    Base.metadata.create_all(engine)

    # Ouvre une session de travail
    with Session(engine) as session:
        # Requête Python — pas une seule ligne de SQL !
        states = session.query(State).order_by(State.id).all()
        for state in states:
            print("{}: {}".format(state.id, state.name))
Utiliser with Session(engine) as session: — la session se ferme automatiquement à la fin du bloc.
Interdit d'utiliser .execute() avec SQLAlchemy dans ce projet. Toujours passer par la Query API.
TABLEAU RÉCAP — MÉTHODES CLÉS
Méthode / Attribut Module Rôle Retourne
MySQLdb.connect() MySQLdb Ouvre une connexion MySQL Objet connexion
conn.cursor() MySQLdb Crée un curseur pour exécuter du SQL Curseur
cur.execute(query) MySQLdb Exécute une requête SQL Nombre de lignes affectées
cur.fetchall() MySQLdb Récupère toutes les lignes du résultat Liste de tuples
create_engine(url) SQLAlchemy Crée le moteur de connexion Engine
Base.metadata.create_all(engine) SQLAlchemy Crée les tables si inexistantes None
session.query(Model) SQLAlchemy Démarre une requête sur un modèle Query object (chaînable)
.filter(condition) SQLAlchemy Filtre les résultats (= WHERE) Query object
.filter_by(attr=val) SQLAlchemy Filtre simple sur un attribut Query object
.order_by(Model.col) SQLAlchemy Trie les résultats (= ORDER BY) Query object
.all() SQLAlchemy Exécute et retourne toutes les lignes Liste d'instances
.first() SQLAlchemy Retourne le premier résultat ou None Instance ou None
.count() SQLAlchemy Compte les lignes (= COUNT) int
session.add(obj) SQLAlchemy Prépare un objet pour insertion None
session.commit() SQLAlchemy Valide les changements en base None
session.delete(obj) SQLAlchemy Marque un objet pour suppression None
BOSS FIGHTS — PROGRESSION
⚔️ BOSS 1 — LVL 1 FACILE : LISTER LES STATES

Objectif : lister tous les states d'une table triés par id avec MySQLdb.

#!/usr/bin/python3
"""Liste tous les states depuis la DB — utilise MySQLdb."""
import MySQLdb
import sys

if __name__ == "__main__":
    conn = MySQLdb.connect(host="localhost", port=3306,
                            user=sys.argv[1], passwd=sys.argv[2],
                            db=sys.argv[3], charset="utf8")
    cur = conn.cursor()
    cur.execute("SELECT * FROM states ORDER BY id ASC")
    for row in cur.fetchall():
        print(row)
    cur.close()
    conn.close()
Résultat : (1, 'California') — chaque ligne est un tuple (id, name).
⚔️ BOSS 2 — LVL 2 INTERMÉDIAIRE : FILTRER PAR NOM

Objectif : avec SQLAlchemy, trouver un state par son nom (insensible à la casse).

#!/usr/bin/python3
"""Cherche un state par nom exact via SQLAlchemy ORM."""
import sys
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from model_state import Base, State

if __name__ == "__main__":
    engine = create_engine(
        'mysql+mysqldb://{}:{}@localhost/{}'.format(*sys.argv[1:4]),
        pool_pre_ping=True
    )
    Base.metadata.create_all(engine)

    with Session(engine) as session:
        # Filtre sur le nom exact — pas de SQL, juste Python
        result = session.query(State).filter(
            State.name == sys.argv[4]
        ).all()

        if result:
            for state in result:
                print("{}: {}".format(state.id, state.name))
        else:
            print("Not found")
💀 BOSS FINAL — LVL 3 HOLBERTON STYLE : INSERT, UPDATE, DELETE

Objectif : créer un nouveau state, le modifier, puis le supprimer — tout sans SQL.

#!/usr/bin/python3
"""CRUD complet sur un State avec SQLAlchemy ORM."""
import sys
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from model_state import Base, State

if __name__ == "__main__":
    engine = create_engine(
        'mysql+mysqldb://{}:{}@localhost/{}'.format(*sys.argv[1:4]),
        pool_pre_ping=True
    )
    Base.metadata.create_all(engine)

    with Session(engine) as session:
        # INSERT — crée un objet Python et l'ajoute à la session
        new_state = State(name="Louisiana")
        session.add(new_state)
        session.commit()
        print("Nouveau state id: {}".format(new_state.id))

        # UPDATE — modifie l'attribut directement, commit enregistre
        state = session.query(State).filter_by(name="Louisiana").first()
        if state:
            state.name = "New Louisiana"
            session.commit()
            print("Modifié : {}".format(state.name))

        # DELETE — marque pour suppression, commit confirme
        to_delete = session.query(State).filter_by(name="New Louisiana").first()
        if to_delete:
            session.delete(to_delete)
            session.commit()
            print("Supprimé.")
Après un add() et commit(), l'objet est "détaché" par défaut. Accéder à new_state.id fonctionne car SQLAlchemy le recharge automatiquement.
ERREURS CLASSIQUES
✗ MAUVAIS
💀 INJECTION SQL
# NE JAMAIS FAIRE — injection SQL possible !
name = sys.argv[1]
cur.execute("SELECT * FROM states WHERE name = '"
           + name + "'")
Si l'utilisateur tape ' OR '1'='1, il lit toute la table. Catastrophique.
✓ CORRECT
✅ PARAMÈTRE SÉCURISÉ
# Utilise les paramètres — MySQLdb échappe automatiquement
name = sys.argv[1]
cur.execute(
    "SELECT * FROM states WHERE name = %s",
    (name,)  # tuple obligatoire, même pour 1 param
)
MySQLdb échappe les caractères dangereux. Ta requête est blindée.
✗ MAUVAIS
💀 OUBLIER DE FERMER
# Connexion qui ne se ferme jamais
conn = MySQLdb.connect(...)
cur = conn.cursor()
cur.execute("SELECT * FROM states")
print(cur.fetchall())
# ← manque cur.close() et conn.close()
Les connexions ouvertes épuisent le pool. Le serveur finit par refuser les nouvelles connexions.
✓ CORRECT
✅ TOUJOURS FERMER
# Ferme dans l'ordre inverse d'ouverture
conn = MySQLdb.connect(...)
cur = conn.cursor()
cur.execute("SELECT * FROM states")
print(cur.fetchall())
cur.close()    # curseur d'abord
conn.close()   # connexion ensuite
Ou utilise un context manager with pour automatiser la fermeture.
✗ MAUVAIS
💀 .execute() AVEC SQLALCHEMY
# Interdit dans ce projet !
session.execute("SELECT * FROM states")
Le projet Holberton interdit explicitement execute() avec SQLAlchemy. Toujours utiliser la Query API.
✓ CORRECT
✅ QUERY API PROPRE
# Requête via l'ORM — zéro SQL brut
states = session.query(State)\
    .order_by(State.id)\
    .all()
for state in states:
    print("{}: {}".format(state.id, state.name))
Chaîne les méthodes : .filter(), .order_by(), .limit(), .all().
✗ MAUVAIS
💀 DOCSTRING MANQUANTE
class State(Base):
    __tablename__ = 'states'
    id = Column(Integer, primary_key=True)
    name = Column(String(128))
Holberton vérifie les docstrings avec python3 -c 'print(State.__doc__)'. Sans docstring = 0 points.
✓ CORRECT
✅ DOCSTRINGS PARTOUT
class State(Base):
    """Modèle représentant la table states."""
    __tablename__ = 'states'
    id = Column(Integer, primary_key=True,
                nullable=False)
    name = Column(String(128), nullable=False)
Module, classe, et chaque fonction doivent avoir une docstring — phrase complète expliquant le but.
RÈGLES CRITIQUES — SKILL TREE
Shebang + docstrings obligatoires

Première ligne : #!/usr/bin/python3. Module, classe, fonction : chacun a sa docstring. Vérifié automatiquement par Holberton.

Paramètres SQL, jamais de concat

Avec MySQLdb, toujours cur.execute("... WHERE x = %s", (val,)). Le tuple en deuxième argument est obligatoire — même pour un seul paramètre.

if __name__ == "__main__": toujours

Le code ne doit pas s'exécuter à l'import. Tout le code principal va dans ce bloc. Testé avec import dans les scripts de validation.

pool_pre_ping=True sur l'engine

Évite les erreurs "MySQL server has gone away" sur les connexions idle. Toujours l'activer quand l'engine est créé.

Zéro .execute() avec SQLAlchemy

Le projet interdit explicitement session.execute(). Passe toujours par session.query(Model).filter(...).all() et ses dérivés.

pycodestyle — style obligatoire

Vérifie ton code avec pycodestyle *.py. Maximum 79 chars par ligne, espaces autour des opérateurs, ligne vide en fin de fichier.

RÉSUMÉ — CARTE MENTALE
🗺️ DEUX APPROCHES EN UN COUP D'ŒIL
╔══════════════════════════════════════════════════════════════╗
║                    PYTHON ORM — RÉSUMÉ                       ║
╠══════════════════════════════════════════════════════════════╣
║  MYSQLDB (bas niveau)         SQLALCHEMY (ORM complet)       ║
║  ──────────────────────       ──────────────────────────     ║
║  1. MySQLdb.connect(...)      create_engine(url, ...)        ║
║  2. conn.cursor()             Base.metadata.create_all()     ║
║  3. cur.execute("SQL", args)  session.query(Model)           ║
║  4. cur.fetchall()              .filter(Model.col == val)    ║
║  5. print(row)                  .order_by(Model.col)         ║
║  6. cur.close()                 .all() / .first() / .count() ║
║  7. conn.close()              session.add(obj) → commit()    ║
║                               session.delete(obj) → commit() ║
╠══════════════════════════════════════════════════════════════╣
║  MODÈLE SQLALCHEMY                                           ║
║  ─────────────────                                           ║
║  Base = declarative_base()                                   ║
║  class Model(Base):                                          ║
║      __tablename__ = 'table_name'                            ║
║      id   = Column(Integer, primary_key=True)                ║
║      name = Column(String(128), nullable=False)              ║
╠══════════════════════════════════════════════════════════════╣
║  RÈGLES HOLBERTON                                            ║
║  ────────────────                                            ║
║  ✓ #!/usr/bin/python3 en ligne 1                             ║
║  ✓ Docstrings : module + classe + chaque fonction            ║
║  ✓ if __name__ == "__main__": pour tout code exécutable      ║
║  ✓ Paramètres %s pour MySQLdb — jamais de concat SQL         ║
║  ✗ .execute() interdit avec SQLAlchemy                       ║
║  ✓ pycodestyle (2.7.*) sans erreurs                          ║
╚══════════════════════════════════════════════════════════════╝