Skip to content

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:

Image

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:

Image

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:
import sgbd_connector

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