18. Escribir código independiente de SGBD
Hemos visto anteriormente que, en algunos casos, era posible migrar fácilmente código Python escrito para SGBD MySQL a código escrito para SGBD PostgreSQL. En este capítulo, mostramos cómo sistematizar este enfoque. La arquitectura propuesta queda así:

Se desea que la elección del conector y, por tanto, del SGBD se realice mediante configuración y no requiera reescribir el script. Recordamos que esto solo es posible en los casos en que el script no utilice extensiones propias del SGBD.
La estructura de los scripts será la siguiente:

Los scripts [any_xx] retoman los scripts ya estudiados para los SGBD, MySQL y PostgreSQL. No vamos a repasarlos todos. Nos centraremos en el script [any_04], que es el más complejo. Recordemos que este script ejecuta los comandos SQL del siguiente archivo [data/commandes.sql]:
# suppression de la table [personnes]
drop table if exists personnes
# création de la table personnes
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# insertion de deux personnes
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# affichage de la table
select prenom, nom, age from personnes
# erreur volontaire
xx
# insertion de trois personnes
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)
# affichage de la table
select prenom, nom, age from personnes
# liste des personnes par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
select nom,prenom from personnes order by nom asc, prenom desc
# liste des personnes ayant un âge dans l'intervalle [20,40] par ordre décroissant de l'âge
# puis à âge égal par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# insertion de mme Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# mise à jour de son âge
update personnes set age=47 where nom='Bruneau'
# liste des personnes ayant Bruneau pour nom
select nom,prenom,age from personnes where nom='Bruneau'
# suppression de Mme Bruneau
delete from personnes where nom='Bruneau'
# liste des personnes ayant Bruneau pour nom
select nom,prenom,age from personnes where nom='Bruneau'
Hemos modificado la línea 2 para que el comando tenga el mismo comportamiento para los SGBD, MySQL y PostgreSQL si la tabla [personnes] no existe.
El script [any_04] se configura mediante el siguiente script [config.py]:
def configure():
import os
# ruta absoluta de la carpeta de este script
script_dir = os.path.dirname(os.path.abspath(__file__))
# configuración de las carpetas de la ruta del sistema
absolute_dependencies = [
# carpetas locales
f"{script_dir}/shared",
]
# configuración de syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# configuración de la aplicación
config = {
# SGBD gestionados
"sgbds": {
"mysql": {
# conector del SGBD
"sgbd_connector": "mysql.connector",
# nombre del módulo que agrupa las funciones de gestión del SGBD
"sgbd_fonctions": "any_module",
# credenciales de conexión
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
},
"postgresql": {
# conector del SGBD
"sgbd_connector": "psycopg2",
# nombre del módulo que agrupa las funciones de gestión del SGBD
"sgbd_fonctions": "any_module",
# identificadores de la conexión
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
}
},
# archivo de comandos SQL
"commands_filename": f"{script_dir}/data/commandes.sql"
}
# Syspath de la aplicación
from myutils import set_syspath
set_syspath(absolute_dependencies)
# se genera el config
return config
La novedad se encuentra en las líneas 18-43:
- línea 20: [sgbds] es un diccionario con dos claves: [mysql] en la línea 21 y [postgresql] en la línea 32;
- el valor asociado a estas claves es un diccionario que proporciona los elementos necesarios para conectarse a un SGBD:
- líneas 21-32: los elementos de una conexión al SGBD MySQL;
- línea 23: el conector Python que se debe utilizar;
- línea 25: el módulo que contiene funciones compartidas;
- líneas 26-30: los identificadores de la conexión;
- líneas 32-41: los mismos elementos para una conexión a SGBD PostgreSQL;
El script [any_04] que ejecuta el archivo de comandos SQL [data/commandes.sql] es el siguiente:
# se configura la aplicación
import config
config = config.configure()
# el syspath está configurado; ya se pueden realizar las importaciones
import importlib
import sys
# verificación de la sintaxis de la llamada
# argv[0] sgbd_name true / false
# se necesitan 3 parámetros
args = sys.argv
erreur = len(args) != 3
if not erreur:
# se recuperan los dos parámetros que nos interesan
sgbd_name = args[1].lower()
with_transaction = args[2].lower()
# verificación de los dos parámetros
erreur = (with_transaction != "true" and with_transaction != "false") \
or sgbd_name not in config["sgbds"].keys()
# ¿error?
if erreur:
print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
sys.exit()
# configuración del sgbd_name
sgbd_config = config["sgbds"][sgbd_name]
# conector del sgbd_name
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
# biblioteca de funciones
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])
# cálculo de un texto a mostrar
with_transaction = with_transaction == "true"
if with_transaction:
texte = "avec transaction"
else:
texte = "sans transaction"
# visualización
commands_filename=config['commands_filename']
print("--------------------------------------------------------------------")
print(f"Exécution du fichier SQL {commands_filename} {texte}")
print("--------------------------------------------------------------------")
# ejecución de las órdenes SQL del archivo
connexion = None
try:
# conexión
connexion = sgbd_connector.connect(
host=sgbd_config['host'],
user=sgbd_config['user'],
password=sgbd_config['password'],
database=sgbd_config['database'])
# ejecución del archivo de comandos 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:
# cierre de la conexión
if connexion:
connexion.close()
# visualización del número de errores
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
# visualización de los errores
for erreur in erreurs:
print(erreur)
Comentarios
- líneas 1-4: se recupera la configuración [config] de la aplicación;
- líneas 10-21: el script se invoca con dos parámetros [sgbd_name with_transaction]:
- [sgbd_name]: el nombre del SGBD que se va a utilizar;
- [with_transaction]: True si se desea ejecutar el archivo de comandos SQL dentro de una transacción, False en caso contrario;
- líneas 10-25: se recuperan y verifican los parámetros;
- línea 28: la configuración del SGBD seleccionado;
- línea 30: se importa el conector del SGBD seleccionado. Para ello se utiliza la biblioteca [importlib] (línea 7), que permite importar un módulo cuyo nombre se encuentra en una variable. El resultado de la operación [importlib.import_module] es un módulo. Así, tras la línea 30, todo ocurre como si la instrucción ejecutada hubiera sido:
Esto nos permitirá escribir en la línea 52 [sgbd_connector.connect], donde se utiliza la función [connect] del módulo [sgbd_connector]. Hay que recordar aquí que [sgbd_connector] es o bien [mysql.connector] o bien [psycopg2]. Estos dos módulos tienen la función [connect]. Del mismo modo, en la línea 60, se puede escribir [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError].
- línea 32: se importa el módulo de funciones utilizadas por el script;
- línea 58: se ejecuta la función [execute_file_of_commands] del módulo de funciones utilizado por el script. En comparación con las versiones anteriores, la firma de esta función tiene un parámetro más, el primero. Se pasa a la función el conector Python [sgbd_connector] que debe utilizar;
- Aparte de estos puntos, el script [any_04] sigue siendo el mismo que en las versiones anteriores;
La biblioteca de funciones [any_module] es la siguiente:
# ---------------------------------------------------------------------------------
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):
…
# inicializaciones
curseur = None
connexion.autocommit = not with_transaction
erreurs = []
try:
# se solicita un cursor
curseur = connexion.cursor()
# ejecución de sql_commands SQL contenidas en sql_commands
# se ejecutan una a una
for command in sql_commands:
# se eliminan los espacios al principio y al final del comando actual
command = command.strip()
# ¿hay un comando vacío o un comentario? Si es así, pasamos al siguiente comando
if command == '' or command[0] == "#":
continue
# ejecución del comando actual
error = None
try:
curseur.execute(command)
except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
error = erreur
# ¿Se ha producido un error?
…
# ---------------------------------------------------------------------------------
def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
…
# procesamiento del archivo SQL
try:
# apertura del archivo en modo lectura
file = open(sql_filename, "r")
# procesamiento
return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
except BaseException as erreur:
# se devuelve una tabla de errores
return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
finally:
pass
El parámetro [sgbd_connector] se ha utilizado en la línea 31 para especificar el tipo de excepciones interceptadas.
La ejecución del script [any_04] con los parámetros [mysql false] da los siguientes resultados:
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