18. Escribir código independiente de SGBD
Anteriormente vimos que, en ciertos casos, era posible migrar fácilmente código en Python escrito para SGBD y MySQL hacia código escrito para SGBD y PostgreSQL. En este capítulo, mostramos cómo sistematizar este enfoque. La arquitectura propuesta es la siguiente:

Se desea que la elección del conector y, por lo 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 analizados para los SGBD, MySQL y PostgreSQL. No los repasaremos 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]:
# eliminación de la tabla [personnes]
drop table if exists personnes
# creación de la tabla «personas»
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# inserción de dos personas
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# visualización de la tabla
select prenom, nom, age from personnes
# error intencional
xx
# inserción de tres personas
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)
# visualización de la tabla
select prenom, nom, age from personnes
# lista de personas ordenadas alfabéticamente por apellido y, en caso de igual apellido, por nombre
select nom,prenom from personnes order by nom asc, prenom desc
# lista de personas cuya edad se encuentra en el intervalo [20,40], ordenadas de mayor a menor según la edad
# y, a continuación, en caso de igual edad, en orden alfabético por apellido y, en caso de igual apellido, en orden alfabético por nombre
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# Inclusión de la Sra. Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# actualización de su edad
update personnes set age=47 where nom='Bruneau'
# lista de personas con el apellido Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
# eliminación de la Sra. Bruneau
delete from personnes where nom='Bruneau'
# lista de personas con el apellido Bruneau
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 syspath
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 administrados
"sgbds": {
"mysql": {
# conector del SGBD
"sgbd_connector": "mysql.connector",
# nombre del módulo que reúne las funciones de administración del SGBD
"sgbd_fonctions": "any_module",
# datos de inicio de sesió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 administración del SGBD
"sgbd_fonctions": "any_module",
# datos de inicio de sesió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 aplica la configuración
return config
La novedad se encuentra en las líneas 18 a 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 de Python que se debe utilizar;
- línea 25: el módulo que contiene las 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()
# la ruta de sistema está configurada; ya se pueden realizar las importaciones
import importlib
import sys
# verificación de la sintaxis de la llamada
# argv[0] sgbd_name verdadero / falso
# 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 para 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: se invoca el script con dos parámetros [sgbd_name with_transaction]:
- [sgbd_name]: el nombre del archivo SGBD que se 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í, después de 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 [mysql.connector] o [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 que utiliza el script;
- línea 58: se ejecuta la función [execute_file_of_commands] del módulo de funciones que utiliza el script. En comparación con las versiones anteriores, la firma de esta función tiene un parámetro más, el primero. Se le pasa a la función el conector de 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 los sql_commands SQL contenidos en sql_commands
# se ejecutan una por una
for command in sql_commands:
# se eliminan los espacios al inicio 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
# ¿Hubo algún 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 de 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 utilizó 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] arroja 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