18. Pisanie kodu niezależnego od SGBD
Wcześniej widzieliśmy, że w niektórych przypadkach możliwe jest proste przeniesienie kodu w języku Python napisanego dla SGBD MySQL do kodu napisanego dla SGBD PostgreSQL. W niniejszym rozdziale pokażemy, jak usystematyzować to podejście. Proponowana architektura wygląda następująco:

Chcemy, aby wybór łącznika, a tym samym SGBD, odbywał się poprzez konfigurację i nie wymagał przepisywania skryptu. Przypominamy, że jest to możliwe tylko w przypadkach, gdy skrypt nie wykorzystuje zastrzeżonych rozszerzeń SGBD.
Struktura drzewa skryptów będzie wyglądać następująco:

Skrypty [any_xx] opierają się na skryptach już omówionych dla SGBD, MySQL i PostgreSQL. Nie będziemy ich wszystkich omawiać. Skoncentrujemy się na skrypcie [any_04], który jest najbardziej złożony. Przypominamy, że skrypt ten wykonuje polecenia SQL z następującego pliku [data/commandes.sql]:
# usunięcie tabeli [personnes]
drop table if exists personnes
# utworzenie tabeli osób
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# dodanie dwóch osób
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# wyświetlenie tabeli
select prenom, nom, age from personnes
# celowy błąd
xx
# dodanie trzech osób
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)
# wyświetlenie tabeli
select prenom, nom, age from personnes
# lista osób w porządku alfabetycznym według nazwisk, a w przypadku identycznych nazwisk – w porządku alfabetycznym według imion
select nom,prenom from personnes order by nom asc, prenom desc
# lista osób w przedziale wiekowym [20,40] w porządku malejącym według wieku
# a następnie, przy tym samym wieku, w porządku alfabetycznym według nazwisk, a przy identycznych nazwiskach – w porządku alfabetycznym według imion
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# dodanie pani Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# aktualizacja jej wieku
update personnes set age=47 where nom='Bruneau'
# lista osób o nazwisku Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
# usunięcie pani Bruneau
delete from personnes where nom='Bruneau'
# lista osób o nazwisku Bruneau
select nom,prenom,age from personnes where nom='Bruneau'
Zmodyfikowaliśmy wiersz 2 tak, aby polecenie zachowywało się identycznie w przypadku skryptów SGBD, MySQL i PostgreSQL, jeśli tabela [personnes] nie istnieje.
Skrypt [any_04] jest konfigurowany przez następujący skrypt [config.py]:
def configure():
import os
# bezwzględna ścieżka do folderu tego skryptu
script_dir = os.path.dirname(os.path.abspath(__file__))
# konfiguracja katalogów w ścieżce systemowej
absolute_dependencies = [
# katalogi lokalne
f"{script_dir}/shared",
]
# ustawianie ścieżki syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# konfiguracja aplikacji
config = {
# obsługiwane systemy zarządzania bazami danych
"sgbds": {
"mysql": {
# konektor systemu zarządzania bazami danych
"sgbd_connector": "mysql.connector",
# nazwa modułu zawierającego funkcje zarządzania systemem baz danych
"sgbd_fonctions": "any_module",
# dane logowania
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
},
"postgresql": {
# konektor systemu zarządzania bazami danych
"sgbd_connector": "psycopg2",
# nazwa modułu zawierającego funkcje zarządzania systemem zarządzania bazami danych
"sgbd_fonctions": "any_module",
# dane logowania
"user": "admpersonnes",
"password": "nobody",
"host": "localhost",
"database": "dbpersonnes"
}
},
# plik poleceń SQL
"commands_filename": f"{script_dir}/data/commandes.sql"
}
# ścieżka systemowa aplikacji
from myutils import set_syspath
set_syspath(absolute_dependencies)
# konfiguracja
return config
Nowości znajdują się w wierszach 18–43:
- wiersz 20: [sgbds] jest słownikiem zawierającym dwa klucze: [mysql] (wiersz 21) oraz [postgresql] (wiersz 32);
- wartością powiązaną z tymi kluczami jest słownik zawierający elementy umożliwiające połączenie z SGBD:
- wiersze 21–32: elementy połączenia z SGBD i MySQL;
- wiersz 23: łącznik Python, którego należy użyć;
- wiersz 25: moduł zawierający funkcje współdzielone;
- wiersze 26–30: identyfikatory połączenia;
- wiersze 32–41: te same elementy dla połączenia z SGBD PostgreSQL;
Skrypt [any_04], który uruchamia plik poleceń SQL [data/commandes.sql], wygląda następująco:
# konfiguracja aplikacji
import config
config = config.configure()
# ścieżka systemowa jest skonfigurowana – można wykonać importy
import importlib
import sys
# sprawdzanie składni wywołania
# argv[0] sgbd_name true / false
# potrzebne są 3 parametry
args = sys.argv
erreur = len(args) != 3
if not erreur:
# pobieramy dwa parametry, które nas interesują
sgbd_name = args[1].lower()
with_transaction = args[2].lower()
# weryfikacja obu parametrów
erreur = (with_transaction != "true" and with_transaction != "false") \
or sgbd_name not in config["sgbds"].keys()
# błąd?
if erreur:
print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
sys.exit()
# konfiguracja urządzenia sgbd_name
sgbd_config = config["sgbds"][sgbd_name]
# złącze sgbd_name
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
# biblioteka funkcji
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])
# obliczanie tekstu do wyświetlenia
with_transaction = with_transaction == "true"
if with_transaction:
texte = "avec transaction"
else:
texte = "sans transaction"
# wyświetlanie
commands_filename=config['commands_filename']
print("--------------------------------------------------------------------")
print(f"Exécution du fichier SQL {commands_filename} {texte}")
print("--------------------------------------------------------------------")
# wykonanie poleceń SQL z pliku
connexion = None
try:
# połączenie
connexion = sgbd_connector.connect(
host=sgbd_config['host'],
user=sgbd_config['user'],
password=sgbd_config['password'],
database=sgbd_config['database'])
# wykonanie pliku poleceń 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:
# zamknięcie połączenia
if connexion:
connexion.close()
# wyświetlenie liczby błędów
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
# wyświetlenie błędów
for erreur in erreurs:
print(erreur)
Komentarze
- wiersze 1–4: pobierana jest konfiguracja aplikacji [config];
- wiersze 10–21: skrypt jest wywoływany z dwoma parametrami [sgbd_name with_transaction]:
- [sgbd_name]: nazwa pliku SGBD, który ma zostać użyty;
- [with_transaction]: True, jeśli plik poleceń SQL ma zostać uruchomiony w ramach transakcji, w przeciwnym razie False;
- wiersze 10–25: parametry są pobierane i sprawdzane;
- wiersz 28: konfiguracja wybranego pliku SGBD;
- wiersz 30: importowany jest łącznik wybranego pliku SGBD. W tym celu wykorzystywana jest biblioteka [importlib] (wiersz 7), która umożliwia importowanie modułu, którego nazwa znajduje się w zmiennej. Wynikiem operacji [importlib.import_module] jest moduł. Tak więc po wierszu 30 wszystko przebiega tak, jakby wykonano instrukcję:
Dzięki temu będziemy mogli zapisać w wierszu 52 [sgbd_connector.connect], gdzie wykorzystujemy funkcję [connect] z modułu [sgbd_connector]. Należy tu pamiętać, że [sgbd_connector] to albo [mysql.connector], albo [psycopg2]. Oba te moduły posiadają funkcję [connect]. Podobnie w wierszu 60 można wpisać [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError].
- w wierszu 32: importuje się moduł funkcji wykorzystywanych przez skrypt;
- w wierszu 58: wywołuje się funkcję [execute_file_of_commands] z modułu funkcji wykorzystywanych przez skrypt. W porównaniu z poprzednimi wersjami sygnatura tej funkcji zawiera o jeden parametr więcej – pierwszy. Do funkcji przekazuje się łącznik Python [sgbd_connector], z którego ma ona korzystać;
- poza tymi zmianami skrypt [any_04] pozostaje taki sam jak w poprzednich wersjach;
Biblioteka funkcji [any_module] wygląda następująco:
# ---------------------------------------------------------------------------------
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):
…
# inicjalizacje
curseur = None
connexion.autocommit = not with_transaction
erreurs = []
try:
# żądanie kursora
curseur = connexion.cursor()
# wykonanie sql_commands SQL zawartych w sql_commands
# są one wykonywane pojedynczo
for command in sql_commands:
# usuwa się spacje na początku i na końcu bieżącego polecenia
command = command.strip()
# czy mamy puste polecenie lub komentarz? Jeśli tak, przechodzimy do następnego polecenia
if command == '' or command[0] == "#":
continue
# wykonanie bieżącego polecenia
error = None
try:
curseur.execute(command)
except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
error = erreur
# czy wystąpił błąd?
…
# ---------------------------------------------------------------------------------
def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
…
# przetwarzanie pliku SQL
try:
# otwarcie pliku w trybie odczytu
file = open(sql_filename, "r")
# przetwarzanie
return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
except BaseException as erreur:
# zwracany jest tabela błędów
return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
finally:
pass
Parametr [sgbd_connector] został użyty w wierszu 31 w celu określenia typu przechwytywanych wyjątków.
Uruchomienie skryptu [any_04] z parametrami [mysql false] daje następujące wyniki:
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