Skip to content

18. Escrever código independente do SGBD

Vimos anteriormente que, em certos casos, era possível migrar facilmente código Python escrito para o SGBD MySQL para código escrito para o SGBD PostgreSQL. Neste capítulo, mostramos como sistematizar esta abordagem. A arquitetura proposta passa a ser a seguinte:

Image

Pretende-se que a escolha do conector e, consequentemente, do SGBD seja feita por configuração e não exija a reescrita do script. Recorde-se que isto só é possível nos casos em que o script não utilize extensões proprietárias do SGBD.

A estrutura dos scripts será a seguinte:

Image

Os scripts [any_xx] retomam os scripts já analisados para os SGBD, MySQL e PostgreSQL. Não vamos repeti-los todos. Vamos concentrar-nos no script [any_04], que é o mais complexo. Recorde-se que este script executa os comandos SQL do ficheiro [data/commandes.sql] seguinte:


# eliminação da tabela [personnes]
drop table if exists personnes
# criação da tabela «pessoas»
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# inserção de duas pessoas
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# visualização da tabela
select prenom, nom, age from personnes
# erro intencional
xx
# inserção de três pessoas
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)
# visualização da tabela
select prenom, nom, age from personnes
# lista de pessoas por ordem alfabética dos apelidos e, em caso de igualdade de apelidos, por ordem alfabética dos nomes próprios
select nom,prenom from personnes order by nom asc, prenom desc
# lista de pessoas com idade no intervalo [20,40], por ordem decrescente de idade
# e, em caso de idades iguais, por ordem alfabética dos apelidos e, em caso de apelidos iguais, por ordem alfabética dos nomes próprios
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# inclusão da Sra. Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# atualização da sua idade
update personnes set age=47 where nom='Bruneau'
# lista das pessoas com o apelido Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
# eliminação da Sra. Bruneau
delete from personnes where nom='Bruneau'
# lista de pessoas com o apelido Bruneau
select nom,prenom,age from personnes where nom='Bruneau'

Alterámos a linha 2 para que o comando tenha o mesmo comportamento para os SGBD, MySQL e PostgreSQL, caso a tabela [personnes] não exista.

O script [any_04] é configurado pelo seguinte script [config.py]:


def configure():
    import os

    # caminho absoluto da pasta deste script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # configuração das pastas do syspath
    absolute_dependencies = [
        # pastas locais
        f"{script_dir}/shared",
    ]

    # definição do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # configuração da aplicação
    config = {
        # SGBDs geridos
        "sgbds": {
            "mysql": {
                # conector do SGBD
                "sgbd_connector""mysql.connector",
                # nome do módulo que reúne as funções de gestão do SGBD
                "sgbd_fonctions""any_module",
                # identificadores de ligação
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            },
            "postgresql": {
                # conector do SGBD
                "sgbd_connector""psycopg2",
                # nome do módulo que reúne as funções de gestão do SGBD
                "sgbd_fonctions""any_module",
                # identificadores da ligação
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            }
        },
        # ficheiro de comandos SQL
        "commands_filename"f"{script_dir}/data/commandes.sql"
    }
    # Syspath da aplicação
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # efetua-se a configuração
    return config

A novidade reside nas linhas 18-43:

  • linha 20: [sgbds] é um dicionário com duas chaves: [mysql] na linha 21 e [postgresql] na linha 32;
  • o valor associado a estas chaves é um dicionário que fornece os elementos necessários para a ligação a um SGBD:
  • linhas 21-32: os elementos de uma ligação ao SGBD MySQL;
    • linha 23: o conector Python a utilizar;
    • linha 25: o módulo que contém funções partilhadas;
    • linhas 26-30: os identificadores da ligação;
  • linhas 32-41: os mesmos elementos para uma ligação ao SGBD PostgreSQL;

O script [any_04] que executa o ficheiro de comandos SQL [data/commandes.sql] é o seguinte:


# configuramos a aplicação
import config

config = config.configure()

# o syspath está configurado — já é possível efetuar as importações
import importlib
import sys

# verificação da sintaxe da chamada
# argv[0] sgbd_name true / false
# são necessários 3 parâmetros
args = sys.argv
erreur = len(args) != 3
if not erreur:
    # recuperamos os dois parâmetros que nos interessam
    sgbd_name = args[1].lower()
    with_transaction = args[2].lower()
    # verificação dos dois parâmetros
    erreur = (with_transaction != "true" and with_transaction != "false") \
             or sgbd_name not in config["sgbds"].keys()
# erro?
if erreur:
    print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
    sys.exit()

# configuração do sgbd_name
sgbd_config = config["sgbds"][sgbd_name]
# conector do sgbd_name
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
# biblioteca de funções
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])


# cálculo de um texto a apresentar
with_transaction = with_transaction == "true"
if with_transaction:
    texte = "avec transaction"
else:
    texte = "sans transaction"

# exibição
commands_filename=config['commands_filename']
print("--------------------------------------------------------------------")
print(f"Exécution du fichier SQL {commands_filename} {texte}")
print("--------------------------------------------------------------------")

# execução das ordens SQL do ficheiro
connexion = None
try:
    # ligação
    connexion = sgbd_connector.connect(
        host=sgbd_config['host'],
        user=sgbd_config['user'],
        password=sgbd_config['password'],
        database=sgbd_config['database'])
    # execução do ficheiro 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:
    # encerramento da ligação
    if connexion:
        connexion.close()

# exibição do número de erros
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
# exibição dos erros
for erreur in erreurs:
    print(erreur)

Comentários

  • linhas 1-4: recupera-se a configuração [config] da aplicação;
  • linhas 10-21: o script é chamado com dois parâmetros [sgbd_name with_transaction]:
    • [sgbd_name]: o nome do SGBD a utilizar;
    • [with_transaction]: True se se pretender executar o ficheiro de comandos SQL no âmbito de uma transação, False caso contrário;
  • linhas 10-25: os parâmetros são recuperados e verificados;
  • linha 28: a configuração do SGBD selecionado;
  • linha 30: importa-se o conector do SGBD selecionado. Para tal, utiliza-se a biblioteca [importlib] (linha 7), que permite importar um módulo cujo nome se encontra numa variável. O resultado da operação [importlib.import_module] é um módulo. Assim, após a linha 30, tudo decorre como se a instrução executada tivesse sido:
import sgbd_connector

Isto vai permitir-nos escrever, na linha 52, [sgbd_connector.connect], onde utilizamos a função [connect] do módulo [sgbd_connector]. É importante lembrar aqui que [sgbd_connector] é ou [mysql.connector] ou [psycopg2]. Estes dois módulos possuem a função [connect]. Da mesma forma, na linha 60, pode escrever-se [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError].

  • linha 32: importa-se o módulo das funções utilizadas pelo script;
  • linha 58: executa-se a função [execute_file_of_commands] do módulo de funções utilizado pelo script. Em comparação com as versões anteriores, a assinatura desta função tem mais um parâmetro, o primeiro. Passa-se à função o conector Python [sgbd_connector] que esta deve utilizar;
  • Para além destes pontos, o script [any_04] mantém-se tal como nas versões anteriores;

A biblioteca de funções [any_module] é a seguinte:


# ---------------------------------------------------------------------------------

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):
    

    # inicializações
    curseur = None
    connexion.autocommit = not with_transaction
    erreurs = []
    try:
        # solicitação de um cursor
        curseur = connexion.cursor()
        # execução dos sql_commands SQL contidos em sql_commands
        # são executados um a um
        for command in sql_commands:
            # eliminam-se os espaços em branco no início e no fim do comando atual
            command = command.strip()
            # trata-se de um comando vazio ou de um comentário? Se sim, passa-se para o comando seguinte
            if command == '' or command[0] == "#":
                continue
            # execução do comando atual
            error = None
            try:
                curseur.execute(command)
            except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
                error = erreur
            # Ocorreu algum erro?
            


# ---------------------------------------------------------------------------------
def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
                             suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
    

    # análise do ficheiro SQL
    try:
        # abertura do ficheiro em modo de leitura
        file = open(sql_filename, "r")
        # processamento
        return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
    except BaseException as erreur:
        # é devolvida uma tabela de erros
        return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
    finally:
        pass

O parâmetro [sgbd_connector] foi utilizado na linha 31 para especificar o tipo das exceções interceptadas.

A execução do script [any_04] com os parâmetros [mysql false] produz os seguintes 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