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

Desejamos que a escolha do conector e, portanto, do SGBD seja feita por meio de configuração e não exija a reescrita do script. Vale lembrar que isso 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:

Os scripts [any_xx] retomam os scripts já analisados para os SGBD, MySQL e PostgreSQL. Não vamos revisá-los todos. Vamos nos concentrar no script [any_04], que é o mais complexo. Vale lembrar que esse script executa os comandos SQL do seguinte arquivo [data/commandes.sql]:
# exclusã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)
# exibiçã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)
# exibição da tabela
select prenom, nom, age from personnes
# lista de pessoas em ordem alfabética pelos sobrenomes e, em caso de sobrenomes iguais, em ordem alfabética pelos nomes próprios
select nom,prenom from personnes order by nom asc, prenom desc
# lista de pessoas com idade no intervalo [20,40], em ordem decrescente de idade
# e, em caso de idades iguais, por ordem alfabética dos sobrenomes e, em caso de sobrenomes 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 idade dela
update personnes set age=47 where nom='Bruneau'
# lista das pessoas com o sobrenome Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
# exclusão da Sra. Bruneau
delete from personnes where nom='Bruneau'
# lista de pessoas com o sobrenome Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
Alteramos 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 do aplicativo
config = {
# SGBDs gerenciados
"sgbds": {
"mysql": {
# conector do SGBD
"sgbd_connector": "mysql.connector",
# nome do módulo que reúne as funções de gerenciamento do SGBD
"sgbd_fonctions": "any_module",
# credenciais de conexã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 gerenciamento do SGDB
"sgbd_fonctions": "any_module",
# identificadores da conexão
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
}
},
# arquivo de comandos SQL
"commands_filename": f"{script_dir}/data/commandes.sql"
}
# Syspath do aplicativo
from myutils import set_syspath
set_syspath(absolute_dependencies)
# fazemos a configuração
return config
A novidade está nas linhas 18 a 43:
- linha 20: [sgbds] é um dicionário com duas chaves: [mysql] na linha 21 e [postgresql] na linha 32;
- o valor associado a essas chaves é um dicionário que fornece os elementos necessários para a conexão a um SGBD:
- linhas 21-32: os elementos de uma conexão com o SGBD MySQL;
- linha 23: o conector Python a ser utilizado;
- linha 25: o módulo que contém funções compartilhadas;
- linhas 26-30: os identificadores da conexão;
- linhas 32-41: os mesmos elementos para uma conexão com o SGBD PostgreSQL;
O script [any_04] que executa o arquivo de comandos SQL [data/commandes.sql] é o seguinte:
# configurando o aplicativo
import config
config = config.configure()
# o syspath está configurado — já é possível fazer as importações
import importlib
import sys
# verificação da sintaxe da chamada
# argv[0] sgbd_name verdadeiro / falso
# 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 ser exibido
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 arquivo
connexion = None
try:
# conexão
connexion = sgbd_connector.connect(
host=sgbd_config['host'],
user=sgbd_config['user'],
password=sgbd_config['password'],
database=sgbd_config['database'])
# execução do arquivo 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 conexã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] do aplicativo;
- linhas 10-21: o script é chamado com dois parâmetros [sgbd_name with_transaction]:
- [sgbd_name]: o nome do SGBD a ser utilizado;
- [with_transaction]: True se for necessário executar o arquivo de comandos SQL dentro 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 isso, utiliza-se a biblioteca [importlib] (linha 7), que permite importar um módulo cujo nome está em uma variável. O resultado da operação [importlib.import_module] é um módulo. Assim, após a linha 30, tudo ocorre como se a instrução executada tivesse sido:
Isso nos permitirá escrever, na linha 52, [sgbd_connector.connect], onde utilizamos a função [connect] do módulo [sgbd_connector]. É preciso lembrar aqui que [sgbd_connector] é ou [mysql.connector] ou [psycopg2]. Esses dois módulos possuem a função [connect]. Da mesma forma, na linha 60, pode-se escrever [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 dessa função possui um parâmetro a mais, o primeiro. Passa-se para a função o conector Python [sgbd_connector] que ela deve utilizar;
- fora desses pontos, o script [any_04] permanece igual ao das 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
# elas são executadas uma a uma
for command in sql_commands:
# eliminam-se os espaços em branco no início e no final 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):
…
# processamento do arquivo SQL
try:
# abertura do arquivo para 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:
# retornando 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