18. Scrivere codice indipendente da SGBD
Abbiamo visto in precedenza che in alcuni casi era possibile migrare facilmente il codice Python scritto per SGBD e MySQL verso codice scritto per SGBD e PostgreSQL. In questo capitolo mostriamo come sistematizzare questo approccio. L’architettura proposta è la seguente:

Si desidera che la scelta del connettore e quindi del SGBD avvenga tramite configurazione e non richieda la riscrittura dello script. Si ricorda che ciò è possibile solo nei casi in cui lo script non utilizzi estensioni proprietarie del SGBD.
La struttura degli script sarà la seguente:

Gli script [any_xx] riprendono gli script già esaminati per i modelli SGBD, MySQL e PostgreSQL. Non li riprenderemo tutti. Ci concentreremo sullo script [any_04], che è il più complesso. Ricordiamo che questo script esegue i comandi SQL del seguente file [data/commandes.sql]:
# eliminazione della tabella [personnes]
drop table if exists personnes
# creazione della tabella persone
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# inserimento di due persone
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# visualizzazione della tabella
select prenom, nom, age from personnes
# errore intenzionale
xx
# inserimento di tre persone
insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)
insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)
insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)
# visualizzazione della tabella
select prenom, nom, age from personnes
# elenco delle persone in ordine alfabetico per cognome e, in caso di cognomi uguali, in ordine alfabetico per nome
select nom,prenom from personnes order by nom asc, prenom desc
# elenco delle persone con età compresa nell'intervallo [20,40] in ordine decrescente di età
# poi, a parità di età, in ordine alfabetico per cognome e, a parità di cognome, in ordine alfabetico per nome
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# inserimento della sig.ra Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# aggiornamento della sua età
update personnes set age=47 where nom='Bruneau'
# elenco delle persone con il cognome Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
# cancellazione della sig.ra Bruneau
delete from personnes where nom='Bruneau'
# elenco delle persone con cognome Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
Abbiamo modificato la riga 2 affinché il comando abbia lo stesso comportamento per SGBD, MySQL e PostgreSQL qualora la tabella [personnes] non esista.
Lo script [any_04] è configurato dallo script [config.py] come segue:
def configure():
import os
# percorso assoluto della cartella di questo script
script_dir = os.path.dirname(os.path.abspath(__file__))
# configurazione delle cartelle del syspath
absolute_dependencies = [
# cartelle locali
f"{script_dir}/shared",
]
# impostazione del syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# configurazione dell'applicazione
config = {
# SGBD gestiti
"sgbds": {
"mysql": {
# connettore del SGBD
"sgbd_connector": "mysql.connector",
# nome del modulo che raggruppa le funzioni di gestione del SGBD
"sgbd_fonctions": "any_module",
# credenziali di accesso
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
},
"postgresql": {
# connettore del DBMS
"sgbd_connector": "psycopg2",
# nome del modulo che raggruppa le funzioni di gestione del DBMS
"sgbd_fonctions": "any_module",
# identificativi di connessione
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
}
},
# file di comandi SQL
"commands_filename": f"{script_dir}/data/commandes.sql"
}
# Syspath dell'applicazione
from myutils import set_syspath
set_syspath(absolute_dependencies)
# si esegue la configurazione
return config
La novità risiede nelle righe 18-43:
- riga 20: [sgbds] è un dizionario con due chiavi: [mysql] alla riga 21 e [postgresql] alla riga 32;
- il valore associato a queste chiavi è un dizionario che fornisce gli elementi necessari per la connessione a un SGBD:
- righe 21-32: gli elementi di una connessione al SGBD MySQL;
- riga 23: il connettore Python da utilizzare;
- riga 25: il modulo contenente le funzioni condivise;
- righe 26-30: gli identificativi della connessione;
- righe 32-41: gli stessi elementi per una connessione a SGBD PostgreSQL;
Lo script [any_04] che esegue il file di comandi SQL [data/commandes.sql] è il seguente:
# si configura l'applicazione
import config
config = config.configure()
# il syspath è configurato - è possibile eseguire le importazioni
import importlib
import sys
# verifica della sintassi della chiamata
# argv[0] sgbd_name true / false
# sono necessari 3 parametri
args = sys.argv
erreur = len(args) != 3
if not erreur:
# si recuperano i due parametri che ci interessano
sgbd_name = args[1].lower()
with_transaction = args[2].lower()
# verifica dei due parametri
erreur = (with_transaction != "true" and with_transaction != "false") \
or sgbd_name not in config["sgbds"].keys()
# errore?
if erreur:
print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
sys.exit()
# configurazione del sgbd_name
sgbd_config = config["sgbds"][sgbd_name]
# connettore del sgbd_name
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
# libreria di funzioni
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])
# calcolo di un testo da visualizzare
with_transaction = with_transaction == "true"
if with_transaction:
texte = "avec transaction"
else:
texte = "sans transaction"
# visualizzazione
commands_filename=config['commands_filename']
print("--------------------------------------------------------------------")
print(f"Exécution du fichier SQL {commands_filename} {texte}")
print("--------------------------------------------------------------------")
# esecuzione dei comandi SQL dal file
connexion = None
try:
# connessione
connexion = sgbd_connector.connect(
host=sgbd_config['host'],
user=sgbd_config['user'],
password=sgbd_config['password'],
database=sgbd_config['database'])
# esecuzione del file di comandi SQL
erreurs = lib.execute_file_of_commands(sgbd_connector, connexion, commands_filename, suivi=True, arrêt=False,
with_transaction=with_transaction)
except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# chiusura della connessione
if connexion:
connexion.close()
# visualizzazione del numero di errori
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
# visualizzazione degli errori
for erreur in erreurs:
print(erreur)
Commenti
- righe 1-4: si recupera la configurazione [config] dell’applicazione;
- righe 10-21: lo script viene richiamato con due parametri [sgbd_name with_transaction]:
- [sgbd_name]: il nome del SGBD da utilizzare;
- [with_transaction]: True se si desidera eseguire il file di comandi SQL all’interno di una transazione, False in caso contrario;
- righe 10-25: i parametri vengono recuperati e verificati;
- riga 28: la configurazione del SGBD selezionato;
- riga 30: si importa il connettore del SGBD selezionato. A tal fine si utilizza la libreria [importlib] (riga 7), che consente di importare un modulo il cui nome è contenuto in una variabile. Il risultato dell’operazione [importlib.import_module] è un modulo. Pertanto, dopo la riga 30, tutto procede come se l’istruzione eseguita fosse stata:
Questo ci consentirà di scrivere alla riga 52 [sgbd_connector.connect], dove si utilizza la funzione [connect] del modulo [sgbd_connector]. Va ricordato che [sgbd_connector] è o [mysql.connector] oppure [psycopg2]. Entrambi questi moduli dispongono della funzione [connect]. Allo stesso modo, alla riga 60, è possibile scrivere [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError].
- riga 32: si importa il modulo delle funzioni utilizzate dallo script;
- riga 58: si esegue la funzione [execute_file_of_commands] del modulo delle funzioni utilizzate dallo script. Rispetto alle versioni precedenti, la firma di questa funzione presenta un parametro in più, ovvero il primo. Alla funzione viene passato il connettore Python [sgbd_connector] che essa deve utilizzare;
- A parte questi punti, lo script [any_04] rimane invariato rispetto alle versioni precedenti;
La libreria di funzioni [any_module] è la seguente:
# ---------------------------------------------------------------------------------
def afficher_infos(curseur):
…
# ---------------------------------------------------------------------------------
def execute_list_of_commands(sgbd_connector, connexion, sql_commands: list,
suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
…
# inizializzazioni
curseur = None
connexion.autocommit = not with_transaction
erreurs = []
try:
# richiesta di un cursore
curseur = connexion.cursor()
# esecuzione dei comandi sql_commands SQL contenuti in sql_commands
# vengono eseguiti uno alla volta
for command in sql_commands:
# si eliminano gli spazi all’inizio e alla fine del comando corrente
command = command.strip()
# si tratta di un comando vuoto o di un commento? Se sì, si passa al comando successivo
if command == '' or command[0] == "#":
continue
# esecuzione del comando corrente
error = None
try:
curseur.execute(command)
except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
error = erreur
# Si è verificato un errore?
…
# ---------------------------------------------------------------------------------
def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
…
# elaborazione del file SQL
try:
# apertura del file in lettura
file = open(sql_filename, "r")
# elaborazione
return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
except BaseException as erreur:
# viene restituito un array di errori
return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
finally:
pass
Il parametro [sgbd_connector] è stato utilizzato alla riga 31 per specificare il tipo delle eccezioni intercettate.
L'esecuzione dello script [any_04] con i parametri [mysql false] fornisce i seguenti risultati:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/anysgbd/any_04.py mysql false
--------------------------------------------------------------------
Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\anysgbd/data/commandes.sql sans transaction
--------------------------------------------------------------------
[drop table if exists personnes] : Exécution réussie
nombre de lignes modifiées : 0
[create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))] : Exécution réussie
nombre de lignes modifiées : 0
[insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
nombre de lignes modifiées : 1
[select prenom, nom, age from personnes] : Exécution réussie
prenom, nom, age,
*****************
('Paul', 'Langevin', 48)
('Sylvie', 'Lefur', 70)
*****************
xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xx' at line 1)
[insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)] : Exécution réussie
nombre de lignes modifiées : 1
[select prenom, nom, age from personnes] : Exécution réussie
prenom, nom, age,
*****************
('Paul', 'Langevin', 48)
('Sylvie', 'Lefur', 70)
('Pierre', 'Nicazou', 35)
('Geraldine', 'Colou', 26)
('Paulette', 'Girond', 56)
*****************
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom, prenom,
************
('Colou', 'Geraldine')
('Girond', 'Paulette')
('Langevin', 'Paul')
('Lefur', 'Sylvie')
('Nicazou', 'Pierre')
************
[select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
nom, prenom, age,
*****************
('Nicazou', 'Pierre', 35)
('Colou', 'Geraldine', 26)
*****************
[insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)] : Exécution réussie
nombre de lignes modifiées : 1
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
nombre de lignes modifiées : 1
[select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
nom, prenom, age,
*****************
('Bruneau', 'Josette', 47)
*****************
[delete from personnes where nom='Bruneau'] : Exécution réussie
nombre de lignes modifiées : 1
[select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
nom, prenom, age,
*****************
*****************
--------------------------------------------------------------------
Exécution terminée
--------------------------------------------------------------------
Il y a eu 1 erreur(s)
xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xx' at line 1)
Process finished with exit code 0