Skip to content

18. Schreiben von DBMS-unabhängigem Code

Wir haben zuvor gesehen, dass es in einigen Fällen möglich war, Python-Code, der für das MySQL-DBMS geschrieben wurde, problemlos auf Code für das PostgreSQL-DBMS zu migrieren. In diesem Kapitel zeigen wir, wie man diesen Ansatz systematisieren kann. Die vorgeschlagene Architektur sieht wie folgt aus:

Image

Wir möchten, dass die Wahl des Konnektors – und damit des DBMS – über die Konfiguration erfolgt und keine Neuprogrammierung des Skripts erfordert. Beachten Sie, dass dies nur in Fällen möglich ist, in denen das Skript keine proprietären DBMS-Erweiterungen verwendet.

Die Verzeichnisstruktur des Skripts sieht wie folgt aus:

Image

Die [any_xx]-Skripte basieren auf den bereits für die DBMS MySQL und PostgreSQL behandelten Skripten. Wir werden nicht auf alle eingehen. Wir konzentrieren uns auf das Skript [any_04], das am komplexesten ist. Beachten Sie, dass dieses Skript die SQL-Befehle aus der folgenden Datei [data/commandes.sql] ausführt:


# suppression de la table [personnes]
drop table if exists personnes
# création de la table personnes
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# insertion de deux personnes
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# affichage de la table
select prenom, nom, age from personnes
# erreur volontaire
xx
# insertion de trois personnes
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)
# affichage de la table
select prenom, nom, age from personnes
# liste des personnes par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
select nom,prenom from personnes order by nom asc, prenom desc
# liste des personnes ayant un âge dans l'intervalle [20,40] par ordre décroissant de l'âge
# puis à âge égal par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# insertion de mme Bruneau
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# mise à jour de son âge
update personnes set age=47 where nom='Bruneau'
# liste des personnes ayant Bruneau pour nom
select nom,prenom,age from personnes where nom='Bruneau'
# suppression de Mme Bruneau
delete from personnes where nom='Bruneau'
# liste des personnes ayant Bruneau pour nom
select nom,prenom,age from personnes where nom='Bruneau'

Wir haben Zeile 2 so geändert, dass sich der Befehl sowohl für MySQL- als auch für PostgreSQL-Datenbankmanagementsysteme gleich verhält, falls die Tabelle [people] nicht existiert.

Das Skript [any_04] wird durch das folgende Skript [config.py] konfiguriert:

def configure():
    import os

    #  absolute path of this script's folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  syspath folder configuration
    absolute_dependencies = [
        #  local files
        f"{script_dir}/shared",
    ]

    #  syspath mounting
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  application configuration
    config = {
        #  managed sgbd
        "sgbds": {
            "mysql": {
                #  sgbd connector
                "sgbd_connector": "mysql.connector",
                #  name of the module containing sgbd management functions
                "sgbd_fonctions": "any_module",
                #  login credentials
                "user": "admpersonnes",
                "password": "nobody",
                "host": "localhost",
                "database": "dbpersonnes"
            },
            "postgresql": {
                #  sgbd connector
                "sgbd_connector": "psycopg2",
                #  name of the module containing sgbd management functions
                "sgbd_fonctions": "any_module",
                #  login credentials
                "user": "admpersonnes",
                "password": "nobody",
                "host": "localhost",
                "database": "dbpersonnes"
            }
        },
        #  command file SQL
        "commands_filename": f"{script_dir}/data/commandes.sql"
    }
    #  application syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    return config

Die neuen Änderungen befinden sich in den Zeilen 18–43:

  • Zeile 20: [sgbds] ist ein Wörterbuch mit zwei Schlüsseln: [mysql] in Zeile 21 und [postgresql] in Zeile 32;
  • der diesen Schlüsseln zugeordnete Wert ist ein Wörterbuch, das die für die Verbindung zu einem DBMS erforderlichen Elemente enthält:
  • Zeilen 21–32: die Elemente für die Verbindung zum MySQL-DBMS;
    • Zeile 23: der zu verwendende Python-Connector;
    • Zeile 25: das Modul, das gemeinsame Funktionen enthält;
    • Zeilen 26–30: die Anmeldedaten für die Verbindung;
  • Zeilen 32–41: dieselben Elemente für eine Verbindung zum PostgreSQL-DBMS;

Das Skript [any_04], das die SQL-Befehlsdatei [data/commandes.sql] ausführt, lautet wie folgt:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
import importlib
import sys

#  check call syntax
#  argv[0] sgbd_name true / false
#  3 parameters are required
args = sys.argv
erreur = len(args) != 3
if not erreur:
    #  we retrieve the two parameters of interest
    sgbd_name = args[1].lower()
    with_transaction = args[2].lower()
    #  check both parameters
    erreur = (with_transaction != "true" and with_transaction != "false") \
             or sgbd_name not in config["sgbds"].keys()
#  mistake?
if erreur:
    print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
    sys.exit()

#  sgbd_name configuration
sgbd_config = config["sgbds"][sgbd_name]
#  sgbd_name connector
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
#  function library
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])


#  calculation of text to be displayed
with_transaction = with_transaction == "true"
if with_transaction:
    texte = "avec transaction"
else:
    texte = "sans transaction"

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

#  execution of SQL orders in the file
connexion = None
try:
    #  connection
    connexion = sgbd_connector.connect(
        host=sgbd_config['host'],
        user=sgbd_config['user'],
        password=sgbd_config['password'],
        database=sgbd_config['database'])
    #  execution of SQL command file
    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:
    #  closing the connection
    if connexion:
        connexion.close()

#  display number of errors
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
#  error display
for erreur in erreurs:
    print(erreur)

Kommentare

  • Zeilen 1–4: Abrufen der Anwendungskonfiguration [config];
  • Zeilen 10–21: Das Skript wird mit zwei Parametern aufgerufen [db_name with_transaction]:
    • [db_name]: der Name des zu verwendenden Datenbankmanagementsystems;
    • [with_transaction]: True, wenn das SQL-Skript innerhalb einer Transaktion ausgeführt werden soll, andernfalls False;
  • Zeilen 10–25: Die Parameter werden abgerufen und überprüft;
  • Zeile 28: Konfiguration des ausgewählten DBMS;
  • Zeile 30: Der Konnektor für das ausgewählte DBMS wird importiert. Dazu wird die Bibliothek [importlib] (Zeile 7) verwendet, die es ermöglicht, ein Modul zu importieren, dessen Name in einer Variablen gespeichert ist. Das Ergebnis der Operation [importlib.import_module] ist ein Modul. Somit verläuft nach Zeile 30 alles so, als wäre die folgende Anweisung ausgeführt worden:
import sgbd_connector

Dies ermöglicht es uns, in Zeile 52 [sgbd_connector.connect] zu schreiben, wo wir die Funktion [connect] des Moduls [sgbd_connector] verwenden. Dabei ist zu beachten, dass [sgbd_connector] entweder [mysql.connector] oder [psycopg2] ist. Beide Module verfügen über die Funktion [connect]. Ebenso können wir in Zeile 60 [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError] schreiben.

  • Zeile 32: Wir importieren das Modul, das die vom Skript verwendeten Funktionen enthält;
  • Zeile 58: Die Funktion [execute_file_of_commands] aus dem Modul, das die vom Skript verwendeten Funktionen enthält, wird aufgerufen. Im Vergleich zu früheren Versionen hat die Signatur dieser Funktion einen zusätzlichen Parameter – den ersten. Wir übergeben den Python-Konnektor [sgbd_connector] an die Funktion, damit sie ihn verwenden kann;
  • Abgesehen von diesen Punkten bleibt das Skript [any_04] gegenüber früheren Versionen unverändert;

Die Funktionsbibliothek [any_module] sieht wie folgt aus:

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

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

    #  initializations
    curseur = None
    connexion.autocommit = not with_transaction
    erreurs = []
    try:
        #  a cursor is requested
        curseur = connexion.cursor()
        #  execution of sql_commands SQL contained in sql_commands
        #  they are executed one by one
        for command in sql_commands:
            #  eliminates blanks at the beginning and end of the current command
            command = command.strip()
            #  is there an empty command or a comment? If so, move on to the next command
            if command == '' or command[0] == "#":
                continue
            #  execute current command
            error = None
            try:
                curseur.execute(command)
            except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
                error = erreur
            #  was there a mistake?
            


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

    #  use of the SQL file
    try:
        #  open file for reading
        file = open(sql_filename, "r")
        #  operation
        return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
    except BaseException as erreur:
        #  an error table is returned
        return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
    finally:
        pass

Der Parameter [sgbd_connector] wurde in Zeile 31 verwendet, um den Typ der abgefangenen Ausnahmen anzugeben.

Die Ausführung des Skripts [any_04] mit den Parametern [mysql false] liefert folgende Ergebnisse:


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