Skip to content

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:

Image

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:

Image

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

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