Table des matières
SQL et Python
Requêtes via un langage de programmation
Utilisation réelle d'une BDD
Nous avons étudié le langage SQL qui permet de gérer une base de données par l'intermédiaire de requêtes : CREATE TABLE, INSERT, UPDATE, SELECT, etc.
Nous avons utilisé un logiciel de gestion nous permettant d’écrire directement les requêtes en langage SQL et de visualiser le contenu de la base un peu comme si nous utilisions un tableur.
Mais ceci n'est pas l'utilisation habituelle d'une base de données.
Si nous reprenons l'exemple de la base de données de films, l'utilisation habituelle serait plutôt celle ci :
- Une fenêtre – par exemple une fenêtre web – met à disposition des boutons permettant à un utilisateur de consulter le contenu de la base ou d'en modifier le contenu.
- Si l'utilisateur souhaite ajouter un acteur, on lui propose un bouton “Ajouter un acteur” qui l'amène à une page adaptée. Sur cette nouvelle page, l'utilisateur trouve des zones de saisie dont la signification est clairement donnée : nom, prénom, date de naissance, biographie…
Cette page peut simplifier la saisie par exemple en proposant un bouton calendrier comme on envoie dans la plupart des sites internet où on doit entrer une date.
Un bouton valider permet de conclure l'ajout. - Si l'utilisateur souhaite faire une recherche de film, on lui propose un champ de recherche dans lequel il écrit le nom du film.
Bref, à aucun moment l'utilisateur n'a à entrer une requête SQL.
Dialogue entre le programme et le SGBD
Le logiciel – par exemple le site internet – se charge de produire les requêtes SQL en fonction des actions de l'utilisateur. Le logiciel doit donc pouvoir dialoguer directement avec le SGBD et lui transmettre des requêtes SQL. Il doit aussi pouvoir recevoir les réponses du SGBD.
- Sur internet, un usage courant est de passer par le langage PHP. Les requêtes http venant des clients et arrivant sur le serveur sont pris en charge par un script PHP, exécuté sur le serveur, et PHP interagit avec un SGBD.
Dans ce cas, le SGBD est souvent MySQL, MariaDB ou PostgreSQL. - Un autre usage qui se développe est de faire exécuter un script en Javascript sur le serveur – au lieu de PHP. Dans ce cas, le SGBD MongoDB est adapté car il est fait pour fonctionner avec Javascript.
- Nous allons étudier le cas de Python.
sqlite3
En Python, on peut utiliser le module sqlite3 – Référence
Importer la bibliothèque
import sqlite3
Connexion à la base de données
On peut ouvrir ou créer un nouveau fichier base de données :
c = sqlite3.connect("example.db")
Ici, la variable c est un objet qui consiste en la connexion au fichier base de données dont le nom est "example.db". Si le fichier n'existe pas, la base est créée.
On peut souhaiter travailler sur une base de données temporaire que l'on n'enregistre pas sur le disque dur. Dans ce cas on cette possibilité :
c = sqlite3.connect(":memory:")
Dans notre cas, nous reprenons le fichier films.db avec mes films et acteurs. Placer ce fichier dans le même répertoire que le script python puis :
c = sqlite3.connect("films.db")
Activation des clés étrangères
La vérification de la cohérence des clé étrangères peut être une lourde charge pour le système. On est donc libre de l'activer ou pas. Si on ne l'active pas, bien sûr, il faudra être plus vigilant pour éviter les incohérences.
Nous souhaitons l'activer, nous ajoutons alors la commande :
c.execute("PRAGMA foreign_keys = 1")
Vous pouvez voir que execute est une méthode de l'objet de connexion c.
Exécution de requêtes SQL
L'exécution des requêtes demande l'utilisation d'un curseur. C'est un objet dont le rôle est justement d'exécuter les requêtes.
cursor = c.cursor()
Le curseur a lui aussi une méthode execute, c'est par elle qu'on lance les requêtes. Exemple :
cursor.execute("CREATE TABLE ARTISTE (idArtiste INTEGER PRIMARY KEY, nom VARCHAR(20), prénom VARCHAR(20), biographie TEXT, naissance DATE);")
Écrit tout à la suite, ce n'est pas très lisible. On préfère généralement utiliser plusieurs lignes :
cursor.execute("""CREATE TABLE ARTISTE (
idArtiste INTEGER PRIMARY KEY,
nom VARCHAR(20),
prénom VARCHAR(20),
biographie TEXT,
naissance DATE
);""")
Les espaces sur la partie gauche n'ont pas d'importance. Ils sont là pour aider la lecture.
Remarques :
- Le
;final de la requête sert dans le cas où on souhaite faire plusieurs requêtes. Si on ne fait qu'une seule requête, le;est inutile. - Avec
sqlite3on ne peut pas faire en une fois plusieurs requête. Si on a 3 requêtes à faire, il faudra appeler 3 fois la méthodescursor.execute.
Exemple d'une insertion
Une insertion de base ne pose pas de problème :
cursor.execute("INSERT INTO ARTISTE(prénom, nom, biographie, naissance) VALUES('Bruce', 'Willis', 'blabla.', '1955-03-19');")
Mais cette situation n'est pas réaliste. En général on sera plutôt dans un cas où nous avons obtenu les informations par ailleurs, depuis une interface web par exemple.
# au moment de faire l'insertion de l'artiste, on nous a fourni # ces variables avec leur valeur prénom = 'Bruce' nom = 'Willis' biographie = 'blabla.' naissance = '1955-03-19'
On sait que la requête voulue est :
INSERT INTO ARTISTE(prénom, nom, biographie, naissance) VALUES('Bruce', 'Willis', 'blabla.', '1955-03-19');
Fausse bonne idée
On se dit que l'on pourrait en Python fabriquer le texte de la requête de cette façon :
texte_requete = "INSERT INTO ARTISTE (prénom, nom, biographie, naissance) VALUES ('{}', '{}', '{}', '{}');".format(prénom, nom, biographie, naissance)
# puis exécution
cursor.execute(texte_requete)
Il ne faut surtout pas faire cela !!!!!
Il serait naturel de faire cela, mais cela pose un gros problème de sécurité appelé injection SQL sur lequel on reviendra plus loin.
Bonne méthode
data = (prénom, nom, biographie, naissance)
cursor.execute("INSERT INTO ARTISTE(prénom, nom, biographie, naissance) VALUES(?, ?, ?, ?);", data)
- Création du tupple contenant les informations à insérer, dans le bon ordre
- Lancement de la requête en prévoyant les trous à compléter sous forme de
?
Cette méthode ressemble à l'utilisation de format mais il y a une différence : elle faite pour éviter l'injection SQL.
id de l'insertion
Quand on insère un nouvel item, on ne précise pas son identifiant, on laisse le SGBD le choisir automatiquement. Mais on peut avoir besoin de l'identifiant de ce que l'on vient d'insérer…
On peut récupérer cet id directement après l'insertion :
id_du_dernier_inséré = cursor.lastrowid
Exemple d'un insertion multiple
On pourrait décider d'insérer plusieurs artistes en même temps.
a_inserer = [
('Bruce', 'Willis', 'blabla', '1955-03-19'),
('Rickman','Alan','blabla...','1946-02-21'),
('Bonnie','Bedelia','blabla','1946-03-25'),
('Gilliam','Terry','blablabla.','1940-11-22')
]
Comment les insérer ? On pourrait passer par une boucle for
for item in a_inserer:
cursor.execute("INSERT INTO ARTISTE(prénom, nom, biographie, naissance) VALUES(?, ?, ?, ?);", item)
Mais on dispose d'une fonction plus adaptée :
cursor.executemany("INSERT INTO ARTISTE(prénom, nom, biographie, naissance) VALUES(?, ?, ?, ?);", a_inserer)
Exemple d'une lecture
Le plus souvent, on consulte le contenu de la base dans la modifier. On utilise pour cela une requête SELECT.
cursor.execute("SELECT * FROM ARTISTE;")
La requête est alors exécutée mais on n'obtient rien.
Les résultats de la sélection sont contenus dans cursor :
premier = cursor.fetchone() # fetch = chercher. On récupère le premier suivant = cursor.fetchone() # récupère le suivant suivant = cursor.fetchone() # encore le suivant # quand on a tout récupéré, fetchone() renvoie None
On peut aussi récupérer les résultats tous d'un coup :
resultats = cursor.fetchall()
Un résultat aura la forme (1, 'Bruce', 'Willis', 'blabla.', '1955-03-19') . Il n'est pas précisé à quoi correspondent les différents champs. Mais comme nous connaissons la table, nous savons que le premier champ est idArtiste, le second est prénom, etc.
Fin des modifications
Pour que les modifications soit prises en compte dans la base de données, on utilise
c.commit()
Et pour refermer la connexion :
c.close()
La fermeture de la connexion de met pas à jour le fichier base de données et il faut donc penser à faire un commit avant la fermeture.
Injection SQL
Il n'est pas obligatoire de comprendre ceci. J'indique l'explication pour les plus avancés d'entre vous.
Imaginons qu'un site permette aux internautes de créer un compte utilisateur. Avec ce compte utilisateur ils peuvent ajouter des items dans la base, par exemple des acteurs. En face chaque acteur qu'ils ont créé, ils voient un bouton avec une poubelle. S'ils appuient dessus, le système envoie une requête au serveur précisant :
- que c'est une demande de suppression,
- l'id de l'item à supprimer, donc l'id de l'artiste.
Supposons que l'utilisateur clique le bouton à côté de Bruce Willis. On aura idArtiste = "1".
Côté serveur, supposons encore que le script ressemble à :
def suppression_artiste(idArtiste):
requete = "DELETE FROM ARTISTE WHERE idArtiste = {}".format(idArtiste)
cursor.execute(requete)
Si l'utilisateur fait les choses normalement, la requête sera bien "DELETE FROM ARTISTE WHERE idArtiste = 1" et tout se passera bien.
Supposons maintenant qu'un utilisateur mal intentionné se crée lui aussi un compte. Puis, comme il est maître de sa propre machine, il force l'envoie d'une requête de suppression mais avec un idArtiste anormal : il donne idArtiste = "1 OR 1 = 1".
Le programme côté serveur fait la même chose et on obtient : "DELETE FROM ARTISTE WHERE idArtiste = 1 OR 1 = 1". Aïe, aïe, aïe !. La condition de WHERE est toujours vraie. Tous les artistes sont supprimés !
Et encore c'est un cas simple. L'utilisateur pourrait écrire pourrait écrire toute une requête, par exemple en envoyant idArtiste = "1; DELETE FROM FILM WHERE 1 = 1".
Nous devons donc empêcher que de telles situations se produisent. L'idée est d'empêcher d'insérer des éléments contenant des commandes SQL. On pourrait le faire manuellement bien sûr, en prévoyant tout un tas de tests pour éliminer tous les exemples de commandes dangereuses. Mais le module fournit déjà toutes les fonctionnalités pour cela. Par exemple, le execute de sqlite se charge de filtrer tous les cas à problème. C'est pourquoi on ne doit surtout pas essayer de compléter la requête nous-même, on doit utiliser les fonctions fournies.
