Accès aux bases de données (MySQL)

Python permet facilement d’interagir avec des systèmes de bases de données relationnelles (SGBDR). Cette facilité est en partie due à la définition d’une API standard d’accès aux bases de données décrite par la PEP 249 (Python Database API Specification v2.0).

Pour accéder à une base de données vous devrez installer avec le module pip le package pour vous connecter au SGBDR de votre choix. La procédure de connexion peut avoir des spécificités selon le SGBDR mais l’interaction avec la base reste standard.

Pour ce chapitre nous prendrons comme exemple MySQL. Nous partirons du principe qu’il existe un serveur MySQL actif sur la machine hôte et qu’il existe un schéma de base de données appelé demo_python. Ce schéma contient une unique table utilisateur créée avec le script suivant :

drop table if exists utilisateur;

create table utilisateur (
    id int primary key auto_increment,
    nom varchar(100) not null,
    score int default 0,
    date_inscription date not null default now(),
    actif bool default FALSE
) engine=innoDB;

Installation du connecteur MySQL

Pour vous connecter à une base de données MySQL, vous devez installer le connecteur MySQL qui est un paquet nommé mysql-connector-python et qui est disponible sur PyPI.

$ python -m pip install mysql-connector-python

Il est fortement recommandé d’installer le connecteur dans un environnement virtuel Python pour éviter d’entrer en conflit avec d’autres projets qui utiliseraient déjà ce connecteur. Pour en savoir plus, consultez le chapitre consacré aux modules pip et venv.

Connexion à la base de données

Le module mysql.connector fournit la méthode connect qui permet de retourner un objet qui représente la connexion vers la base de données. Vous devez fournir les paramètres host, user et password pour donner l’adresse du SGBDR, le login et le mot de passe de connexion. Vous pouvez également fournir le paramètre database pour indiquer quelle base de données vous souhaitez utiliser.

import mysql.connector

db = mysql.connector.connect(
    host="localhost",
    user="login",
    password="mot_de_passe",
    database="demo_python"
)

# faire quelque chose d'utile avec la connexion

db.close()

La connexion à la base de données se comporte comme un gestionnaire de ressource. Cela signifie que nous pouvons employer la syntaxe with pour nous assurer de fermer la connexion correctement. De plus, il est utile d’employer l’opérateur ** pour faire un unpack de dictionnaire. Ainsi nous pouvons représenter les paramètres de connexion comme un simple dictionnaire :

import mysql.connector

connection_params = {
    'host': "localhost",
    'user': "login",
    'password': "mot_de_passe",
    'database': "demo_python",
}

with mysql.connector.connect(**connection_params) as db :
    # faire quelque chose d'utile avec la connexion
    pass

L’interaction avec la base de données se fera à travers un curseur. Cet objet permet à la fois d’envoyer des requêtes et de consulter les résultats quand il s’agit d’une requête de consultation de données. Pour créer un curseur, on appelle la méthode cursor() sur la connexion. Il est recommandé de fermer un curseur lorsqu’il n’est plus utilisé. Comme un curseur est également un gestionnaire de ressource, nous pouvons également employer la syntaxe with :

import mysql.connector

connection_params = {
    'host': "localhost",
    'user': "login",
    'password': "mot_de_passe",
    'database': "demo_python",
}

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        # faire quelque chose d'utile avec le curseur
        pass

Pour exécuter une requête, nous utilisons la méthode execute du curseur.

Insertion de données

L’insertion de données se fait avec une requête SQL de type insert.

1
2
3
4
5
with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute("insert into utilisateur (nom, score, actif) \
                   values ('david', 10, TRUE)")
        db.commit()

Notez à la ligne 5, l’appel à la méthode commit() de la connexion qui permet de valider les modifications. Si vous n’appelez pas cette méthode, la transaction avec la base de données ne sera pas terminée et aucune ligne ne sera insérée en base de données.

Note

Pour annuler toutes les modifications effectuées dans une transaction, vous pouvez appeler la méthode rollback() de la connexion.

Note

Le connecteur MySQL fournit la propriété non standard autocommit sur la connexion. Si cette propriété vaut True alors un commit est automatiquement fait après chaque exécution de requête.

1
2
3
4
5
with mysql.connector.connect(**connection_params) as db :
    db.autocommit = True
    with db.cursor() as c:
        c.execute("insert into utilisateur (nom, score, actif) \
                   values ('david', 10, TRUE)")

Il est souvent plus utile de fournir les valeurs à insérer sous la forme de paramètres. Il suffit de remplacer les valeurs par %s dans la requête et de passer en deuxième paramètre à la méthodes execute un n-uplet avec les valeurs.

request = """insert into utilisateur
             (nom, score, actif)
             values (%s, %s, %s)"""
params = ("david", 10, True)

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request, params)
        db.commit()

Remarquez que le curseur se débrouille pour convertir les types Python en types SQL et nous pouvons directement passer des chaînes de caractères, des nombres, des valeurs booléennes et même des dates.

import datetime

request = """insert into utilisateur
             (nom, score, actif, date_inscription)
             values (%s, %s, %s, %s)"""
params = ("david", 10, True, datetime.date.today())

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request, params)
        db.commit()

La méthode executemany permet d’insérer plusieurs lignes en passant un tableau pour la valeur des paramètres :

request = """insert into utilisateur
             (nom, score, actif)
             values (%s, %s, %s)"""
params = [
    ("david", 10, True),
    ("laurence", 15, True),
    ("julien", 12, False)
]

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.executemany(request, params)
        db.commit()

Si vous voulez connaître le nombre de lignes insérées par l’exécution de la requête, vous pouvez consulter la propriété rowcount du curseur :

request = """insert into utilisateur
             (nom, score, actif)
             values (%s, %s, %s)"""
params = [
    ("david", 10, True),
    ("laurence", 15, True),
    ("julien", 12, False)
]

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.executemany(request, params)
        db.commit()
        print("Nombre de lignes insérées :", c.rowcount)

Sélection de données

Pour récupérer des données depuis la base de données, il suffit de passer un requête SQL de type select en paramètre de la méthode execute du curseur et ensuite d’appeler la méthode fetchall() pour récupérer une liste de n-uplets contenant les résultats.

request = "select id, nom, score, date_inscription, actif from utilisateur"

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request)
        resultats = c.fetchall()
        for utilisateur in resultats:
            print(utilisateur)

L’exécution du code précédent affichera :

(1, 'david', 10, datetime.date(2021, 1, 17), 1)
(2, 'laurence', 15, datetime.date(2021, 1, 17), 1)
(3, 'julien', 12, datetime.date(2021, 1, 17), 0)

On voit que les résultats sont bien des n-uplets et que le connecteur MySQL a réalisé une conversion de type sauf pour la colonne actif qui est un nombre au lieu d’être une valeur booléenne. Cela n’est pas trop grave car nous avons vu que True et False peuvent presque être considérés comme les nombres 1 et 0.

Note

La conversion imparfaite pour les valeurs booléennes ne vient pas de Python mais de MySQL. En effet, le type BOOLEAN n’existe pas vraiment en MySQL, il s’agit plus d’un alias pour une colonne de type entier.

Récupérer les données en flux

La méthode fetchall() est simple à utiliser mais peut poser un problème de performance. En effet, cette méthode retourne une liste, cela signifie que tous les résultats sont récupérés de la base de données pour être convertis en n-uplets. S’il y a un nombre important de lignes dans le résultat de la requête, cela signifie que la liste peut être très grande et avoir une empreinte mémoire importante.

Parfois, on désire traiter directement la donnée retournée et il n’est pas nécessaire de la stocker dans une liste. Nous pouvons améliorer notre code en optant pour l’appel à la méthode fetchone(). Cette méthode retourne un seul résultat sous la forme d’un n-uplet. Lorsqu’il n’y a plus de résultat à lire, la méthode retourne None. Nous pouvons revoir notre code :

request = "select id, nom, score, date_inscription, actif from utilisateur"

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request)
        while True:
            utilisateur = c.fetchone()
            if utilisateur is None:
                break
            print(utilisateur)

Le résultat du programme sera le même que précédemment sauf que les résultats sont extraits un à un et qu’aucune liste n’est créée.

Prudence

Si vous fermez un curseur avant d’avoir extrait tous les résultats, vous obtiendrez une exception. N’utilisez pas la méthode fetchone() pour ne retourner que le premier résultat, elle n’a pas été conçue pour cela. Son rôle est de permettre de retourner tous les résultats d’une requête mais un à un.

Si vous voulez limiter le nombre de résultats retournés par une requête, utilisez l’instruction SQL limit :

select id, nom, score, date_inscription, actif from utilisateur limit 1

La méthode fetchmany() est un compromis entre fetchone() et fetchall(). En effet, si fetchall() peut avoir un impact sur la consommation mémoire, fetchone() peut avoir un impact sur les performances car les aller-retour entre le programme et la base de données ont un coût en terme de temps.

La méthode fetchmany() prend en paramètre le nombre maximum de résultats qu’il faut aller chercher lors de cette appel. Lorsqu’il n’y a plus de résultat, la méthode retourne une liste vide.

request = "select id, nom, score, date_inscription, actif from utilisateur"

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request)
        while True:
            resultats = c.fetchmany(10)
            if not resultats:
                break
            for utilisateur in resultats:
                print(utilisateur)

Si ce code paraît difficile à écrire, il est assez facile de créer une fonction pour simplifier une fois pour toute notre travail :

def fetch_from_database(cursor, request, callback, chunck_size=10):
    """
        Utilise le cursor pour exécuter la requête SQL donnée par request.
        callback est une méthode qui est appelée pour chaque résultat extrait.
        Elle recevra en paramètre le tuple des valeurs des colonnes.
        chunck_size indique le nombre max de résultats récupérés en une fois.
    """
    cursor.execute(request)
    while True:
        resultats = c.fetchmany(chunck_size)
        if not resultats:
            break
        for colonnes in resultats:
            callback(colonnes)

Et le programme devient alors :

request = "select id, nom, score, date_inscription, actif from utilisateur"

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        fetch_from_database(c, request, print)

Utilisation de paramètres dans les conditions where

Comme pour les requêtes d’insertion, il est utile de spécifier des paramètres dans une requête SELECT pour les valeurs d’une clause where.

Astuce

Ce mécanisme nous prémunit de l’injection SQL car la structure de la requête est fournie quelle que soit la valeur des paramètres.

Les paramètres sont spécifiés par %s dans la requête et leurs valeurs sont données par un n-uplet passé comme second paramètre à la méthode execute :

request = "select id, nom, score, date_inscription, actif \
           from utilisateur where nom = %s"

params = ("david",)

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request, params)
        resultats = c.fetchall()
        for utilisateur in resultats:
            print(utilisateur)

Modification des données

Toutes les autres requêtes SQL de modification suivent le même principe que les requêtes d’insertion. On utilise la méthode execute d’un curseur. Il faut appeler la méthode commit() de la connexion pour valider la transaction. Enfin, il est possible de passer des paramètres à la requête et de connaître le nombre de lignes impactées par la requête grâce à la propriété rowcount.

request = "update utilisateur \
           set actif = %s \
           where nom = %s"

params = (False, "david")

with mysql.connector.connect(**connection_params) as db :
    with db.cursor() as c:
        c.execute(request, params)
        db.commit()
        print("Nombre de lignes mises à jour :", c.rowcount)