Skip to content

19. Verwendung von ORM und SQLALCHEMY

Im vorigen Kapitel wurde gezeigt, dass man in bestimmten Fällen Code schreiben kann, der unabhängig vom verwendeten SGBD ist, und zwar mit der folgenden Architektur:

Image

In diesem Kapitel werden wir den ORM (Objekt-Relational-Mapper) [sqlalchemy] verwenden, um unabhängig vom verwendeten SGBD einheitlich auf die SGBD zuzugreifen. Ein ORM ermöglicht zwei Dinge:

  • Es ermöglicht einem Skript, mit dem SGBD zu kommunizieren, ohne SQL-Befehle auszuführen;
  • es verbirgt die Besonderheiten jedes einzelnen SGBD vor dem Skript;

Die Architektur sieht nun wie folgt aus:

Das Skript ist nun durch das ORM von den Konnektoren getrennt. Es kommuniziert mit dem ORM über Klassen und Methoden. Es führt keinen SQL-Code aus. Dies übernimmt das ORM mithilfe der Konnektoren, mit denen es verbunden ist. Es verbirgt die Besonderheiten dieser Konnektoren vor dem Skript. Daher ist der Skriptcode unempfindlich gegenüber einem Wechsel des Konnektors (also des SGBD);

Die Struktur der untersuchten Skripte sieht wie folgt aus:

Image

19.1. Installation von ORM und [sqlalchemy]

Das ORM [sqlalchemy] liegt in Form eines Python-Pakets vor, das in einem Python-Terminal installiert werden muss:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\sqlalchemy>pip install sqlalchemy
Collecting sqlalchemy
  Downloading SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl (1.2 MB)
     || 1.2 MB 3.3 MB/s
Installing collected packages: sqlalchemy
Successfully installed sqlalchemy-1.3.18

19.2. Skripte 01: Die Grundlagen

Image

  • in [1], die Skripte, die untersucht werden sollen. Diese Skripte verwenden die Klassen aus [2]: BaseEntity, MyException, Person, Utils;

19.2.1. Konfiguration

Die Datei [config] konfiguriert die Anwendung wie folgt:


def configure():
    # root_dir
    # Absoluter Pfad als Referenz für die relativen Pfade der Konfiguration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # Absolute Pfade der Abhängigkeiten
    absolute_dependencies = [
        # BaseEntity, MyException, Person, Utils
        f"{root_dir}/classes/02/entities",
    ]

    # Der Syspath wird festgelegt
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Konfiguration der Klassen
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # Konfiguration wird übernommen
    return {}

Kommentare

  • Zeile 8: Der Ordner, der die Klassen [BaseEntity, MyException, Personne, Utils] enthält, wird in den Python-Pfad aufgenommen;
  • Zeilen 12–13: Der Python-Pfad der Anwendung wird festgelegt;
  • Zeilen 16–17: Man erinnert sich vielleicht daran, dass die Klasse |BaseEntity| ein Klassenattribut namens [excluded_keys] hat. Dieses Attribut ist eine Liste, in die man die Eigenschaften der Klasse aufnimmt, die nicht im Wörterbuch der Klasse erscheinen sollen (Funktion asdict). Hier wird die Eigenschaft **[\_sa\_instance\_state] aus dem Zustand der Klasse **[Personne] ausgeschlossen. Wir werden bald sehen, warum;

19.2.2. Skript [démo]

Das Skript [démo] zeigt eine erste Anwendung von ORM und [sqlalchemy]:


# Die Anwendungskonfiguration wird abgerufen
import config

config = config.configure()

# Importe
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper

from Personne import Personne

# Metadaten
metadata = MetaData()

# die Tabelle
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )
# die Klasse „Person“ vor dem Mapping
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")

# das Mapping
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'Vorname: personnes_table.c.prenom,
    'Nachname: personnes_table.c.nom,
    'Alter: personnes_table.c.age
})

# Person 1 wurde nicht geändert
print(f"personne1={personne1.__dict__}")
# Die Klasse „Person“ wurde geändert – sie wurde „erweitert“
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

Kommentare

  • Zeilen 1–4: Die Anwendung wird konfiguriert;
  • Zeilen 6–10: Die für das Skript erforderlichen Module werden importiert;
  • Zeile 13: [MetaData] ist eine Klasse von [sqlalchemy];
  • Zeilen 15–22: [Table] ist eine Klasse von [sqlalchemy]. Sie ermöglicht die Beschreibung einer Datenbanktabelle. Hier beschreiben wir die Tabelle [personnes] aus der Datenbank MySQL [dbpersonnes], die im Kapitel |MySQL| behandelt wurde;
    • Zeile 16: Der erste Parameter [personnes] ist der Name der beschriebenen Tabelle;
    • Zeile 16: Der zweite Parameter [metadata] ist die in Zeile 13 erstellte Instanz [MetaData];
    • Zeilen 17–22: Jeder der folgenden Parameter beschreibt eine Spalte der Tabelle mit einer für [sqlalchemy] spezifischen Syntax, die jedoch der Syntax von SQL ähnelt;
    • Jede Spalte wird durch eine Instanz der Klasse [Column] von [sqlalchemy] beschrieben;
      • Der erste Parameter ist der Name der Spalte;
      • der zweite Parameter ist ihr Typ;
      • die folgenden Parameter sind benannte Parameter:
        • Zeile 17: [primary_key=True], um anzugeben, dass die Spalte [id] der Primärschlüssel der Tabelle [personnes] ist;
        • Zeile 18: [nullable=False], um anzugeben, dass eine Spalte zwingend einen Wert haben muss, wenn eine Zeile in die Tabelle eingefügt wird;
    • Zeile 21: Schließlich ermöglicht die Klasse [UniqueConstraint] die Beschreibung einer Eindeutigkeitsbeschränkung. Hier wird festgelegt, dass die Spalten „nom“ und „prénom“ in der Tabelle eindeutig sein müssen. Mit der Eigenschaft [name] kann dieser Einschränkung ein Name zugewiesen werden. Hier sind zwei Fälle zu unterscheiden:
      • Man beschreibt eine bereits vorhandene Tabelle. In diesem Fall muss man den Namen der Einschränkung in den Eigenschaften der Tabelle suchen (phpMyAdmin oder pgAdmin);
      • Man beschreibt eine Tabelle, die noch angelegt werden soll. In diesem Fall gibt man den gewünschten Namen ein;
  • Zeilen 23–25: Man legt eine Person mit dem Namen [personne1] an und zeigt ihr Wörterbuch [__dict__] an. Hier erhalten wir:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • Zeilen 27–33: Wir führen ein Mapping durch, d. h., wir erstellen eine Zuordnung zwischen der Klasse [Personne] und der Tabelle [personnes]. Im Wesentlichen handelt es sich um eine Zuordnung [propriétés de la classe  colonnes de la table]. Die Funktion [mapper] akzeptiert hier drei Parameter:
    • Zeile 28: Der erste Parameter ist der Name der Klasse, für die das Mapping durchgeführt wird;
    • Zeile 28: Der zweite Parameter ist die Tabelle, der sie zugeordnet wird. Dabei handelt es sich um das in Zeile 16 angelegte Objekt [Table];
    • Zeile 28: Der dritte Parameter ist hier ein Parameter mit dem Namen [properties]. Dabei handelt es sich um ein Wörterbuch, in dem die Schlüssel die Eigenschaften der zugeordneten Klasse und die Werte die Spalten der zugeordneten Tabelle sind. Um die Spalte X der Tabelle [personnes_table] zu bezeichnen, schreibt man [personnes_table.c.X];
  • Zeilen 35–36: Nach Abschluss des Mappings wird die Person [personne1] erneut angezeigt. Es ist festzustellen, dass sie sich nicht verändert hat:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • Zeilen 37–39: Es wird eine neue Person [personne2] angelegt und angezeigt. Man erhält dann folgende Anzeige:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}

Man stellt fest, dass das Wörterbuch [__dict__] grundlegend geändert wurde:

  • (Fortsetzung)
    • Es erscheint eine neue Eigenschaft [_sa_instance_state]. Man sieht, dass es sich um ein Objekt von ORM und [sqlalchemy] handelt;
    • die anderen Eigenschaften wurden von ihrem Präfix befreit, das angab, zu welcher Klasse sie gehörten;

Daraus lässt sich schließen, dass der Mapping-Vorgang in den Zeilen 27–33 die Klasse [Personne] verändert hat.

Wenn man den Status eines Objekts [Personne] anzeigen möchte, benötigt man in der Regel die Eigenschaft [_sa_instance_state] nicht. Sie dient nämlich nur der internen Verarbeitung von [sqlalchemy] und ist für uns im Allgemeinen nicht von Interesse. Deshalb haben wir im Skript [config] Folgendes geschrieben:


    # Konfiguration der Klassen
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

19.2.3. Das Skript [main]

Das Skript [main] bearbeitet die Tabelle [personnes] der Datenbank MySQL [dbpersonnes], indem es mit [sqlalchemy] interagiert. Um den weiteren Verlauf zu verstehen, muss man sich die hier verwendete Architektur vor Augen führen:

Image

Wenn [Database1] die Basis [dbpersonnes] ist, sieht man, dass die Verbindung zwischen dem Skript und dieser Basis über zwei Entitäten verläuft:

  • den Python-Konnektor zu SGBD MySQL;
  • die Entitäten SGBD und MySQL;

Das Skript [main] kommuniziert mit dem ORM, das wiederum mit dem Python-Konnektor kommuniziert. Das Skript ORM kommuniziert mit diesem Konnektor mithilfe der in den Abschnitten |MySQL| und |PostgreSQL| beschriebenen Tools, insbesondere durch das Senden von Befehlen SQL. Das Skript [main] verwendet keine Befehle SQL. Es stützt sich auf die API (Application Programming Interface) des ORM, die aus Klassen und Schnittstellen besteht.

Das Skript [main] lautet wie folgt:


# Die Anwendung wird konfiguriert
import config

config = config.configure()

# Importe
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.exc import IntegrityError, InterfaceError
from sqlalchemy.orm import mapper, sessionmaker

from Personne import Personne

# Verbindungszeichenfolge zu einer Datenbank MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

# Metadaten
metadata = MetaData()

# die Tabelle
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )

# das Mapping
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'Vorname: personnes_table.c.prenom,
    'Nachname: personnes_table.c.nom,
    'Alter: personnes_table.c.age
})

# die Session-Factory
Session = sessionmaker()
Session.configure(bind=engine)

session = None
try:
    # eine Sitzung
    session = Session()

    # Löschen der Tabelle [personnes]
    session.execute("drop table if exists personnes")

    # Neuerstellung der Tabelle anhand des Mappings
    metadata.create_all(engine)

    # ein Einfügen
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

    # eine Abfrage
    personnes = session.query(Personne).all()

    # Anzeige
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # zwei weitere Einfügungen, von denen die zweite aufgrund der Eindeutigkeit (Vorname, Nachname) fehlschlägt
    session.add(Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 10}))
    session.add(Personne().fromdict({"id": 69, "prénom": "x1", "nom": "y1", "âge": 10}))

    # eine Abfrage
    personnes = session.query(Personne).all()

    # Anzeige
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # Sitzungsvalidierung
    session.commit()

except (InterfaceError, IntegrityError) as erreur:
    # Anzeige
    print(f"L'erreur suivante s'est produite : {erreur}")
    # Abbruch der letzten Sitzung
    if session:
        print("rollback...")
        session.rollback()
finally:
    # Freigabe der Ressourcen der Sitzung
    if session:
        session.close()

Kommentare

  • Zeilen 1–4: Die Anwendung wird konfiguriert;
  • Zeilen 7–9: Es wird eine ganze Reihe von Klassen und Schnittstellen aus der Bibliothek [sqlalchemy] importiert;
  • Zeile 11: Die Klasse [Personne] wird importiert;
  • Zeile 14: Die Verbindungszeichenfolge zur Datenbank. Sie gibt an:
    • den verwendeten SGBD (mysql);
    • den verwendeten Python-Konnektor (mysql.connector ohne den Punkt);
    • den Benutzer, der die Verbindung herstellt (admpersonnes);
    • sein Passwort (nobody);
    • den Rechner, auf dem sich das SGBD befindet (localhost = Rechner, auf dem das Skript ausgeführt wird);
    • der Name der Datenbank (dbpersonnes);

Mit diesen Informationen kann sich [sqlalchemy] mit der Datenbank verbinden. Beachten Sie, dass der verwendete Python-Konnektor bereits installiert sein muss. [sqlalchemy] übernimmt dies nicht.

  • Zeilen 19–26: Beschreibung der Tabelle [personnes];
  • Zeilen 28–34: Zuordnung zwischen der Klasse [Personne] und der Tabelle [personnes];
  • Zeilen 36–38: Die meisten Vorgänge vom Typ [sqlalchemy] werden in einer Sitzung ausgeführt. Der Begriff „Sitzung“ ([sqlalchemy]) ist mit dem Begriff „Transaktion“ (SQL) vergleichbar. Die Sitzungen werden anhand der Klasse [Session] erstellt, die von der Funktion [sessionmaker] in Zeile 37 zurückgegeben wird;
  • Zeile 38: Die Klasse [Session] wird über die Verbindungszeichenfolge in Zeile 14 mit der Datenbank [dbpersonnes] verknüpft;
  • Zeile 43: Es wird eine Sitzung angelegt. Wie bereits erwähnt, lässt sich eine Sitzung mit einer Transaktion vergleichen;
  • Zeilen 45–46: Mit der Methode [Session.execute] lässt sich ein Befehl SQL ausführen. Dies ist nicht üblich, da bereits erwähnt wurde, dass mit ORM die Verwendung der Sprache SQL vermieden werden kann;
  • Zeilen 48–49: Mit der Methode [metadata.create_all] lassen sich alle Tabellen anlegen, die die Instanz [MetaData] aus Zeile 17 verwenden. Wir haben nur eine: die Tabelle [personnes], die in den Zeilen 20–26 definiert ist. [sqlalchemy] nutzt die Informationen aus diesen Zeilen, um die Tabelle anzulegen. Hier liegt ein erster Vorteil von ORM: Es verbirgt die Besonderheiten der SGBD. Tatsächlich kann sich die Reihenfolge von SQL zu [create] von einem SGBD zum anderen aufgrund der den Spalten zugewiesenen Datentypen stark unterscheiden. Es gab keine Vereinheitlichung der Datentypen. Daher variiert die Reihenfolge von einem SGBD zum anderen. Hier beschreiben wir dank [sqlalchemy]:
    • beschreiben wir die gewünschte Tabelle eindeutig;
    • [sqlalchemy] generiert das passende [create] für das vorliegende SGBD;
  • Zeile 52: Ein Objekt [Personne] wird der Sitzung hinzugefügt. Dadurch wird es nicht automatisch in die Datenbank eingefügt. Tatsächlich folgt ein ORM seinen eigenen Regeln, um sich mit der Datenbank zu synchronisieren. Es versucht stets, die Anzahl der von ihm ausgeführten Abfragen zu optimieren. Nehmen wir ein Beispiel. Das Skript fügt (add) zwei Personen (Person1, Person2) zur Sitzung hinzu und führt anschließend eine Abfrage durch: Es möchte alle in der Tabelle vorhandenen Personen anzeigen. [sqlalchemy] kann dabei wie folgt vorgehen:
    • Das Hinzufügen von [personne1] kann im Arbeitsspeicher erfolgen. Es ist vorerst nicht erforderlich, diese Person in die Datenbank einzutragen;
    • das Gleiche gilt für [personne2];
    • Als Nächstes folgt die Abfrage vom Typ [select]. Dazu müssen alle Zeilen der Tabelle [personnes] abgerufen werden. [sqlalchemy] speichert daraufhin [personne1, personne2] in der Datenbank und führt anschließend die Abfrage durch;

[sqlalchemy] führt somit für den Entwickler transparente Optimierungen durch.

  • Zeile 56: Um eine Abfrage vom Typ [select] (ich möchte … anzeigen) durchzuführen, wird die Methode [Session.query] verwendet. Der Parameter der Methode [query] ist die Klasse, die der abgefragten Tabelle zugeordnet ist. Diese Methode gibt einen Typ [Query] zurück. Die Methode [Query.all] fordert alle Objekte vom Typ [Personne] der Sitzung an. Als Rückgabewert erhält sie alle Zeilen der Tabelle [personnes], jeweils in Form eines Objekts vom Typ [Personne]. Dazu nutzt [sqlalchemy] das Mapping, das zwischen der Klasse [Personne] und der Tabelle [personnes] erstellt wurde. Das Ergebnis von Zeile 56 ist eine Liste von Objekten vom Typ [Personne];
  • Zeilen 58–61: Die Elemente der Liste [personnes] werden angezeigt. Da die Klasse [Personne] von der Klasse [BaseEntity] abgeleitet ist, ist die hier in Zeile 61 implizit verwendete Methode [Personne.__str__] tatsächlich die Methode [BaseEntity.__str__], die die Zeichenkette jSON des aufrufenden Objekts zurückgibt. Diese Zeichenkette ist die Zeichenkette jSON aus dem Wörterbuch [Personne.asdict] (siehe |BaseEntity|). Wir haben gesagt, dass nach dem Mapping in jedem Objekt [Personne] die Eigenschaft [_sa_instance_state] zu finden sein wird. Der Wert dieser Eigenschaft ist jedoch kein Typ [BaseEntity]. Sie muss daher aus dem Wörterbuch der Klasse [Personne] ausgeschlossen werden, da sonst die Anzeige „abstürzt“. Dies wurde im Skript [config] umgesetzt;
  • Zeilen 63–65: Es werden zwei weitere Personen hinzugefügt, die denselben Vor- und Nachnamen haben. Es besteht jedoch eine Eindeutigkeitsbeschränkung für die Vereinigung dieser beiden Spalten. Daher sollte ein Fehler auftreten. Genau das wollen wir überprüfen;
  • Zeilen 67–68: Wir fordern erneut die Liste aller Personen aus der Datenbank an;
  • Zeilen 70–73: Und diese werden angezeigt;
  • Zeilen 75–76: Die Sitzung wird mit „Commit“ bestätigt. Wie der Name schon sagt, wird die zugrunde liegende Transaktion bestätigt;
  • Bei der Ausführung wird sich zeigen, dass die Zeilen 67–76 aufgrund der in Zeile 65 ausgelösten Ausnahme nicht ausgeführt werden. Wir springen dann zu den Zeilen 78–84, um die Ausnahme zu behandeln;
  • Zeile 78: Die Ausnahme [InterfaceError] tritt auf, wenn [sqlalchemy] keine Verbindung zur Datenbank [dbpersonnes] herstellen kann. Die Ausnahme [IntegrityError] tritt in Zeile 65 auf;
  • Zeile 80: Der Fehler wird angezeigt;
  • Zeilen 82–84: Wenn die Sitzung existiert, wird sie abgebrochen. Dies entspricht dem Abbruch der zugrunde liegenden Transaktion;
  • Zeilen 85–88: In jedem Fall, ob Fehler vorliegt oder nicht, wird die Sitzung geschlossen, um Ressourcen freizugeben;

Die Ergebnisse der Ausführung lauten wie folgt:


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/sqlalchemy/01/main.py
Liste des personnes ---------
{"nom": "y", "prénom": "x", "id": 67, "âge": 10}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y1-x1' for key 'uix_1'
[SQL: INSERT INTO personnes (id, prenom, nom, age) VALUES (%(id)s, %(prenom)s, %(nom)s, %(age)s)]
[parameters: ({'id': 68, 'prenom': 'x1', 'nom': 'y1', 'age': 10}, {'id': 69, 'prenom': 'x1', 'nom': 'y1', 'age': 10})]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...

Process finished with exit code 0
  • Zeilen 2–3: Die Liste der Personen nach dem ersten Eintrag;
  • Zeile 5: Die Ausnahme [IntegrityError], die auftrat, als zwei Personen mit demselben Vor- und Nachnamen hinzugefügt wurden;
  • Zeilen 6–7: Zu beachten ist der Auftrag SQL, der fehlgeschlagen ist. Es handelt sich um einen konfigurierten Befehl INSERT: [sqlalchemy] hat die beiden Personen mit einem einzigen Befehl INSERT eingefügt. Hier sieht man, dass er versucht hat, die ausgegebenen Aufträge SQL zu optimieren;

Sehen wir uns nun mit phpMyAdmin den Inhalt der Tabelle [personnes] an:

Image

In [6] sieht man, dass die Tabelle leer ist. Es ist nicht einmal die erste Person vorhanden, die das Skript in die Sitzung aufgenommen hatte. Dies liegt daran, dass die Sitzung in einer Transaktion stattfand und diese in der Klausel [except] des Skripts [main] rückgängig gemacht wurde.

Nehmen wir nun folgende Änderung in [main] vor:


    # Einfügen
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

Nachdem wir in Zeile 2 eine Person hinzugefügt haben, heben wir die Auskommentierung in Zeile 3 auf. Der Vorgang [session.commit] validiert die zugrunde liegende Transaktion, und eine neue Transaktion wird gestartet. Nach der Ausführung sieht der Inhalt der Tabelle [personnes] wie folgt aus:

Image

In [6] ist zu sehen, dass der erste Eintrag beibehalten wurde. Dies liegt daran, dass er innerhalb einer Transaktion 1 vorgenommen wurde und der darauf folgende Fehler innerhalb einer Transaktion 2 auftrat.

19.3. Skripte 02: Die Zuordnungen von [sqlalchemy]

Image

Die Skripte 02 sind eine Variante der Skripte 01. Wir versuchen, möglichst viele Konfigurationen in [config.py] vorzunehmen. Dort konfigurieren wir nun die Anwendungsumgebung [sqlalchemy]:


def configure():
    # Absoluter Pfad als Referenz für die relativen Pfade der Konfiguration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # Absolute Pfade der Abhängigkeiten
    absolute_dependencies = [
        # BaseEntity, MyException, Person, Utils
        f"{root_dir}/classes/02/entities",
    ]

    # Der Syspath wird festgelegt
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Importe
    from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
    from sqlalchemy.orm import mapper, sessionmaker

    # Verbindung zu einer Datenbank MySQL
    engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

    # Metadaten
    metadata = MetaData()

    # die Tabelle
    personnes_table = Table("personnes", metadata,
                            Column('id', Integer, primary_key=True),
                            Column('prenom', String(30), nullable=False),
                            Column("nom", String(30), nullable=False),
                            Column("age", Integer, nullable=False),
                            UniqueConstraint('nom''prenom', name='uix_1')
                            )

    # das Mapping
    from Personne import Personne

    mapper(Personne, personnes_table, properties={
        'id': personnes_table.c.id,
        'Vorname: personnes_table.c.prenom,
        'Nachname: personnes_table.c.nom,
        'Alter: personnes_table.c.age
    })

    # die Session-Factory
    Session = sessionmaker()
    Session.configure(bind=engine)

    # Diese Informationen werden in die Konfiguration aufgenommen
    config = {}
    config["Session"] = Session
    config["metadata"] = metadata
    config["engine"] = engine
    config["personnes_table"] = personnes_table

    # Konfiguration der Klassen
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # die Konfiguration wird bereitgestellt
    return config

Kommentare

  • Zeilen 2–12: Konfiguration des Python-Pfads;
  • Zeilen 14–45: Konfiguration der Umgebung [sqlalchemy];
  • Zeilen 47–52: Die Umgebung [sqlalchemy] wird in das Konfigurationswörterbuch aufgenommen;
  • Zeilen 54–56: Die Klasse [Personne] wird konfiguriert;

Mit dieser Konfiguration sieht das Skript [main] wie folgt aus:


# Die Anwendung wird konfiguriert
import config

config = config.configure()

# Der Syspath ist konfiguriert – man führt die Importe durch
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError

from Personne import Personne

session = None
try:
    # eine Sitzung
    session = config["Session"]()

    # Löschen der Tabelle [personnes]
    session.execute("drop table if exists personnes")

    # Neuerstellung der Tabelle anhand des Mappings
    config["metadata"].create_all(config["engine"])

    # zwei Einfügungen
    session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
    personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
    session.add(personne)

    # Validierung der beiden Einfügungen
    session.commit()

    # eine Abfrage
    personnes = session.query(Personne).all()

    # Anzeige
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # zwei weitere Einfügungen, von denen die zweite fehlschlägt
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))

    # eine Abfrage
    personnes = session.query(Personne).all()

    # Anzeige
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # Sitzungsvalidierung
    session.commit()

except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
    # Anzeige
    print(f"L'erreur suivante s'est produite : {erreur}")
    # Abbruch der letzten Sitzung
    if session:
        print("rollback...")
        session.rollback()
finally:
    # Anzeige
    print("Travail terminé...")
    # Freigabe der Ressourcen der Sitzung
    if session:
        session.close()

Die Ergebnisse der Ausführung lauten wie folgt:


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/sqlalchemy/02/main.py
Liste des personnes-----------
{"âge": 10, "nom": "y", "prénom": "x", "id": 1}
{"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
[SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
[parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Travail terminé...

Process finished with exit code 0

In phpMyAdmin hat sich die Tabelle [personnes] wie folgt verändert:

Image

Sehen wir uns nun die von [sqlalchemy] generierte Tabelle [personnes] an:

Image

  • In [6] sind die für die verschiedenen Spalten verwendeten Typen aufgeführt;
  • in [7] ist zu sehen, dass die Spalte [id] das Attribut [AUTO_INCREMENT] hat. Das bedeutet, dass beim Einfügen einer Zeile in die Tabelle, falls diese Zeile keinen Wert für die Spalte [id] enthält, dieser von MySQL inkrementell generiert wird: 1, 2, 3, … Dank dieser Eigenschaft müssen wir uns beim Einfügen in die Tabelle keine Gedanken über den Wert des Primärschlüssels machen: Wir lassen ihn von MySQL generieren;
  • In [8] sehen wir, dass die Spalte [id] der Primärschlüssel ist;
  • in [9] finden wir die Eindeutigkeitsbeschränkung für die Felder [nom, prenom];

19.4. Skripte 03: Bearbeitung der Entitäten der Sitzung [sqlalchemy]

Image

Die Konfigurationsdatei [config] ist dieselbe wie im vorherigen Beispiel. Im Skript [main] werden die klassischen Operationen [INSERT, UPDATE, DELETE, SELECT] an der Tabelle [personnes] mithilfe der Methoden von [sqlalchemy] durchgeführt:


# Konfiguration der Anwendung
import config

config = config.configure()

# Importe
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne

# Zeigt den Inhalt der Tabelle an [personnes]
def affiche_table(session: Session):
    print("----------------")
    # eine Abfrage
    personnes = session.query(Personne).all()
    # Anzeige
    affiche_personnes(personnes)

# zeigt eine Liste von Personen an
def affiche_personnes(personnes: list):
    print("----------------")
    # Anzeige
    for personne in personnes:
        print(personne)


# main ---------------------------
session = None
try:
    # eine Sitzung
    session = config["Session"]()

    # Löschen der Tabelle [personnes]
    # checkfirst=True: Prüft zunächst, ob die Tabelle existiert
    config["personnes_table"].drop(config["engine"], checkfirst=True)

    # Neuerstellung der Tabelle anhand des Mappings
    config["metadata"].create_all(config["engine"])

    # Einfügungen
    session.add(Personne().fromdict({"prénom": "Pierre", "nom": "Nicazou", "âge": 35}))
    session.add(Personne().fromdict({"prénom": "Géraldine", "nom": "Colou", "âge": 26}))
    session.add(Personne().fromdict({"prénom": "Paulette", "nom": "Girondé", "âge": 56}))

    # Der Inhalt der Sitzung wird angezeigt
    affiche_table(session)

    # Liste der Personen in alphabetischer Reihenfolge der Nachnamen und bei gleichen Nachnamen in alphabetischer Reihenfolge der Vornamen
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # Anzeige
    affiche_personnes(personnes)

    # Liste der Personen, deren Alter im Intervall [20,40] liegt, in absteigender Reihenfolge nach dem Alter
    # anschließend bei gleichem Alter in alphabetischer Reihenfolge der Nachnamen und bei gleichen Nachnamen in alphabetischer Reihenfolge der Vornamen
    personnes = session.query(Personne). \
        filter(Personne.âge >= 20, Personne.âge <= 40). \
        order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())

    # Anzeige
    affiche_personnes(personnes)

    # Eintragung von Frau Bruneau
    bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
    session.add(bruneau)
    # Änderung ihres Alters
    bruneau.âge = 47

    # Liste der Personen mit dem Nachnamen Bruneau
    personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()

    # Anzeige
    affiche_personnes([personne])

    # Löschen von Frau Bruneau
    session.delete(personne)

    # Liste der Personen mit dem Nachnamen Bruneau
    personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")

    # Anzeige
    affiche_personnes(personnes)

    # Sitzung bestätigen
    session.commit()

except (DatabaseError, InterfaceError, IntegrityError) as erreur:
    # Anzeige
    print(f"L'erreur suivante s'est produite : {erreur}")
    # Abbruch der letzten Sitzung
    if session:
        session.rollback()

finally:
    # Anzeige
    print("Travail terminé...")
    # Freigabe der Ressourcen der Sitzung
    if session:
        session.close()

Kommentare

  • Zeilen 20–25: Die Funktion [affiche_personnes] zeigt die Elemente einer Personenliste an;
  • Zeilen 12–18: Die Funktion [affiche_table] zeigt den Inhalt der Tabelle [personnes] an;
  • Zeilen 34–36: Die Tabelle [personnes] wird gelöscht. Im Gegensatz zu früheren Versionen wird kein Befehl SQL verwendet, sondern eine Methode von [sqlalchemy]:
    • config["personnes_table"] ist das Objekt [Table], das die Tabelle [personnes] beschreibt;
    • config["engine"] ist die Verbindungszeichenfolge zur Datenbank [dbpersonnes];
    • Der Parameter mit dem Namen [checkfirst=True] legt fest, dass der Vorgang nur ausgeführt wird, wenn die Tabelle [personnes] existiert;
  • Zeilen 38–39: Die Tabelle [personnes] wird neu angelegt;
  • Zeilen 41–44: Drei Personen werden in die Sitzung aufgenommen. Es sei daran erinnert, dass sie nicht unbedingt sofort in die Tabelle [personnes] eingefügt werden. Dies hängt von der auf Leistung ausgerichteten Strategie von [sqlalchemy] ab;
  • Zeilen 46–47: Der Inhalt der Tabelle [personnes] wird angezeigt. Falls die Einfügungen der drei Personen noch nicht erfolgt waren, werden sie nun aufgrund dieser Abfrage durchgeführt;
  • Zeilen 49–50: Ein Anwendungsbeispiel für die Methode [order_by], mit der die Ergebnisse einer Abfrage in einer bestimmten Reihenfolge dargestellt werden können. Die Syntax [order_by(critère1, critère2)] zeigt die Ergebnisse zunächst nach dem Kriterium [critère1] an, und wenn Zeilen denselben Wert für [critère1] aufweisen, werden sie anschließend nach dem Kriterium [critère2] sortiert. Man kann mehrere Kriterien wie folgt festlegen:
  • Zeilen 55–59: Einführung des Filterkonzepts mit der Methode [filter]. Die Notation [filter(critère1, critère2)] stellt eine logische Verknüpfung (ET) zwischen den verwendeten Kriterien her;
  • Zeilen 64–67: Eine neue Person wird angemeldet;
  • Zeilen 70–71: Ein weiteres Beispiel für eine gefilterte Abfrage. Die Funktion [func.lower(param)] wandelt [param] in Kleinbuchstaben um. Es stehen somit weitere Funktionen zur Verfügung, die mit [func.xx] bezeichnet sind. Im Ausdruck in Zeile 71:
    • [session.query.filter] gibt eine Liste von Objekten [Personne] zurück;
    • [session.query.filter.first] gibt das erste Element dieser Liste zurück;
  • Zeile 77: Ein Element wird aus der Sitzung entfernt;
  • Zeile 86: Die Sitzung wird bestätigt;

Die Ergebnisse der Ausführung lauten wie folgt:

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/sqlalchemy/03/main.py
----------------
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0
  • Zeilen 4–6: der Inhalt der Sitzung;
  • Zeilen 8–10: der Inhalt der Sitzung in absteigender Reihenfolge der Namen;
  • Zeilen 12–13: Inhalt der Sitzung für Personen, deren Alter im Intervall [20, 40] liegt;
  • Zeile 15: die Person mit dem Namen „bruneau“;

In phpMyAdmin lautet der Inhalt der Tabelle [personnes] am Ende der Ausführung wie folgt:

Image

19.5. Skripte 04: Verwendung einer Datenbank [PostgreSQL]

Image

Der Ordner [04] ist eine Kopie des Ordners [03]. Es wird nur eine einzige Änderung vorgenommen, nämlich die Verbindungszeichenfolge in der Datei [config]:


    # Verbindung zu einer Datenbank PostgreSQL
    engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")

Diese Verbindungszeichenfolge verweist nun auf die Datenbank [dbpersonnes] eines SGBD [PostgreSQL]. Beachten Sie die Verwendung des Konnektors [psycopg2]. Dieser muss installiert sein.

Die Ausführung des Skripts [main] 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/sqlalchemy/04/main.py
----------------
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0

Mit dem Tool [pgAdmin] (siehe Abschnitt |pgAdmin|) weist die Tabelle [personnes] folgenden Status auf:

Image

Die Tabelle [personnes] wurde mit dem folgenden Code SQL generiert:

Image

  • In [4-5] ist ersichtlich, dass die Spalte [id] der Primärschlüssel ist. Außerdem ist zu erkennen, dass sie einen Standardwert [mot clé DEFAULT] hat, was dazu führt, dass beim Einfügen einer Zeile ohne Primärschlüssel dieser durch den Wert SGBD generiert wird. Dies ist eine gängige Vorgehensweise: Man lässt SGBD die Primärschlüssel generieren;

Diese Version 05 der Skripte [sqlalchemy] zeigt deutlich, wie einfach der Wechsel von einem SGBD zum anderen ist: Es genügte, die Verbindungszeichenfolge in einem Konfigurationsskript zu ändern. Ansonsten hat sich nichts geändert. Vergleicht man die Spaltentypen von [id, nom, prenom, age] oben mit denen der Tabelle MySQL aus Beispiel |02|, so stellt man fest, dass sie unterschiedlich sind. [sqlalchemy] passt sie an das verwendete SGBD an. Diese einfache Anpassungsfähigkeit an ein neues SGBD ist Grund genug, [sqlalchemy] oder ein anderes ORM zu verwenden.

19.6. Skripte 05: Vollständiges Beispiel

Image

Das untersuchte Beispiel ist eine Wiederaufnahme des im Abschnitt |troiscouches-v01| behandelten Beispiels. Dieses Beispiel zeigte eine dreischichtige Architektur [ui, métier, dao], die Entitäten [Classe, Elève, Matière, Note] verarbeitete. Die Entitäten waren fest in einer Schicht [dao] codiert. Wir speichern sie nun in einer Datenbank. Wir verwenden zwei SGBD: MySQL und PostgreSQL.

19.6.1. Die Architektur der Anwendung

Die Architektur der Anwendung sieht wie folgt aus:

Image

  • In [1-3] finden sich die Ebenen [ui, métier, dao], die bereits im Beispiel |troiscouches-v01| vorhanden sind. Die Ebene [dao] kommuniziert nun mit der Ebene [ORM];
  • die Schichten [1-5] werden durch Python-Code implementiert;

19.6.2. Die Datenbanken

Wir erstellen eine Datenbank mit dem Namen MySQL, die dem Benutzer [admecole] gehört und das Passwort [mdpecole] hat. Dazu befolgen wir die im Abschnitt |Erstellung einer Datenbank| beschriebene Vorgehensweise:

Image

Image

  • in [1] die Datenbank [dbecole] ohne die Tabellen [3];
  • in [7] hat der Benutzer [admecole] alle Berechtigungen für diese Datenbank;

Das Gleiche gilt für die Datenbanken SGBD und PostgreSQL. Wir erstellen eine Datenbank mit dem Namen [dbecole], die dem Benutzer [admecole] gehört, dessen Passwort [mdpecole] lautet. Dazu befolgen wir die im Abschnitt |Erstellung einer Datenbank| beschriebene Vorgehensweise:

Image

  • in [1] die Datenbank [dbecole];
  • in [2], der Benutzer [admecole];
  • in [3-4] gehört die Datenbank [dbecole] dem Benutzer [admecole];

19.6.3. Die von der Anwendung verarbeiteten Entitäten

In der Anwendung |troiscouches v01| wurden folgende Entitäten verarbeitet (siehe |Entitäten|). Genau diese Entitäten werden in den oben genannten Datenbanken gespeichert. Wir werden diese Entitäten in der neuen Anwendung nicht duplizieren, sondern sie dort abrufen, wo sie bereits definiert sind.

Die Klasse [Classe]:


# Importe
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # aus dem Klassenstatus ausgeschlossene Attribute
    excluded_keys = []

    # Eigenschaften der Klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # ID: Kennung der Klasse
        # Name: Name der Klasse
        return BaseEntity.get_allowed_keys() + ["nom"]

    # Getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    # Setter
    @nom.setter
    def nom(self: object, nom: str):
        # name muss eine nicht leere Zeichenkette sein
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

Die Klasse [Elève]:


# Importe
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # aus dem Klassenstatus ausgeschlossene Attribute
    excluded_keys = []

    # Eigenschaften der Klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # ID: Kennung des Schülers
        # Nachname: Nachname des Schülers
        # Vorname: Vorname des Schülers
        # Klasse: Klasse des Schülers
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # Getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    # Setter
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # „Name“ muss eine nicht leere Zeichenkette sein
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        # Vorname muss eine nicht leere Zeichenkette sein
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            # Es wird ein Klassentyp erwartet
            if isinstance(value, Classe):
                self.__classe = value
            # oder ein Typ „dict“
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # oder ein Typ „json“
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

Die Klasse [Matière]:


# Importe
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # Attribute, die aus dem Zustand der Klasse ausgeschlossen sind
    excluded_keys = []

    # Eigenschaften der Klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # ID: Kennung des Fachs
        # Name: Name des Fachs
        # Koeffizient: Koeffizient des Fachs
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # Getter
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    # Setter
    @nom.setter
    def nom(self: object, nom: str):
        # Der Name muss eine nicht leere Zeichenkette sein
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        # Der Koeffizient muss eine reelle Zahl >= 0 sein
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # Fehler?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Die Klasse [Note]:


# Importe
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # Aus dem Klassenstatus ausgeschlossene Attribute
    excluded_keys = []

    # Eigenschaften der Klasse
    @staticmethod
    def get_allowed_keys() -> list:
        # ID: Kennung der Note
        # Wert: die Note selbst
        # Schüler: Schüler (vom Typ „Schüler“), auf den sich die Note bezieht
        # Fach: Fach (vom Typ „Fach“), auf das sich die Note bezieht
        # Das Objekt „Note“ ist somit die Note eines Schülers in einem Fach
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # Getter
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # Getter
    @valeur.setter
    def valeur(self: object, valeur: float):
        # Die Note muss eine reelle Zahl zwischen 0 und 20 sein
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # Es wird der Typ „Schüler“ erwartet
            if isinstance(value, Elève):
                self.__élève = value
            # oder vom Typ „dict“
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # oder vom Typ „json“
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            # Es wird ein Typ „Fach“ erwartet
            if isinstance(value, Matière):
                self.__matière = value
            # oder einen Typ „dict“
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # oder einen JSON-Typ
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

19.6.4. Konfiguration

Image

Die Konfiguration wurde auf mehrere Dateien aufgeteilt:

  • die allgemeine Konfiguration in [config.py]: Sie legt den Python-Pfad der Anwendung fest und instanziiert die Schichten der Architektur;
  • die Konfiguration von [sqlalchemy] in [config_database]: Hier werden die Zuordnungen zwischen Klassen und Tabellen vorgenommen;
  • die Schichten der Anwendung werden in [config_layers] konfiguriert;

Die Datei [config] sieht wie folgt aus:


def configure(config: dict) -> dict:
    import os

    # Schritt 1 ---
    # Der Python-Pfad der Anwendung wird festgelegt
    # Absoluter Pfad zum Ordner dieses Skripts
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # Absoluter Pfad als Referenz für die relativen Pfade der Konfiguration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # Absolute Pfade der Abhängigkeiten
    absolute_dependencies = [
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # Dreischicht-Projekt v01
        f"{root_dir}/troiscouches/v01/interfaces",
        f"{root_dir}/troiscouches/v01/services",
        f"{root_dir}/troiscouches/v01/entities",
        # Unterlagen zum vorliegenden Projekt
        script_dir,
        f"{script_dir}/../services",
    ]

    # Aktualisierung des Syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Schritt 2 ------
    # Datenbankkonfiguration
    import config_database
    config = config_database.configure(config)

    # Schritt 3 ------
    # Instanziierung der Anwendungsschichten
    import config_layers
    config = config_layers.configure(config)

    # Konfiguration wird übernommen
    return config
  • Zeilen 4–27: Erstellung des Python-Pfads der Anwendung;
  • Zeilen 29–32: Konfiguration von [sqlalchemy];
  • Zeilen 34–37: Konfiguration der Anwendungsschichten;

Die Datei „[config_database]“ sieht wie folgt aus:


def configure(config: dict) -> dict:
    # Konfiguration: ['sgbd'] ist der Name des verwendeten SGBD
    # MySQL: MySQL
    # PostgreSQL: PostgreSQL

    # SQLAlchemy-Konfiguration
    from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine

    from sqlalchemy.orm import mapper, relationship, sessionmaker

    # Verbindungsstrings zu den verwendeten Datenbanken
    engines = {
        'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
        'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
    }
    # Verbindungszeichenfolge zur genutzten Datenbank
    engine = create_engine(engines[config['sgbd']])

    # Metadaten
    metadata = MetaData()

    # die Tabellen der Datenbank
    tables = {}
    # die zugeordneten Klassen
    from Classe import Classe
    from Elève import Elève
    from Note import Note
    from Matière import Matière

    # die Klassentabelle
    tables['classes'] = classes_table = \
        Table("classes", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              )

    mapper(Classe, tables['classes'], properties={
        'ID: classes_table.c.id,
        'Name: classes_table.c.nom
    })

    # die Schülertabelle
    tables['élèves'] = élèves_table = \
        Table("élèves", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('prénom', String(30), nullable=False),
              # Ein Schüler gehört zu einer Klasse
              Column('classe_id', Integer, ForeignKey('classes.id')),
              )
    # Zuordnung
    mapper(Elève, tables['élèves'], properties={
        'id': élèves_table.c.id,
        'Nachname: élèves_table.c.nom,
        'Vorname: élèves_table.c.prénom,
        'Klasse: relationship(Klasse, backref="Schüler", lazy="select")
    })

    # das Inhaltsverzeichnis
    tables['matières'] = matières_table = \
        Table("matières", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('coefficient', Float, nullable=False)
              )
    # Zuordnung
    mapper(Matière, tables['matières'], properties={
        'id': matières_table.c.id,
        'Name: matières_table.c.nom,
        "coefficient": matières_table.c.coefficient
    })

    # die Notentabelle
    tables['notes'] = notes_table = \
        Table("notes", metadata,
              Column('id', Integer, primary_key=True),
              Column('valeur', Float, nullable=False),
              # Eine Note ist die eines Schülers
              Column('élève_id', Integer, ForeignKey('élèves.id')),
              # Eine Note bezieht sich auf ein Fach
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # Zuordnung
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'Wert: notes_table.c.valeur,
        'Schüler': Beziehung(Schüler, backref="Noten", lazy="select"),
        '„Fach“: Beziehung(Fach, backref="Noten", lazy="select")
    })

    # Konfiguration der Entitäten [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

    # die Session-Factory
    Session = sessionmaker()
    Session.configure(bind=engine)

    # eine Sitzung
    session = Session()

    # Bestimmte Informationen werden im Konfigurationswörterbuch gespeichert
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # die Konfiguration wird bereitgestellt
    return config

Anmerkungen

  • Zeilen 1–4: Die Funktion [configure] erhält ein Wörterbuch als Parameter. Es wird nur der Schlüssel [sgbd] verwendet. Er lautet [mysql], wenn die Datenbank eine MySQL-Datenbank ist, und [pgres], wenn die Datenbank eine PostgreSQL-Datenbank ist;
  • Zeilen 6–9: Import von Elementen aus [sqlalchemy]. Das Skript [config_database] führt die Zuordnungen zwischen den Tabellen der Datenbank [dbecole] und den Entitäten [Classes, Elève, Matière, Note] durch. In der Tabelle sind die Daten der Entität in einer Zeile gekapselt. Im Python-Code sind sie in einem Objekt gekapselt. Daher der Name ORM (Object Relational Mapper): Das Skript ORM stellt eine Zuordnung (eine Verbindung) zwischen den Zeilen einer relationalen Datenbank und Objekten her. In dieser Anwendung haben wir vier Entitäten [Classe, Elève, Matière, Note], die mit vier Tabellen [classes, élèves, matières, notes] verknüpft werden. Beachten Sie, dass die Tabellennamen Zeichen mit Akzenten enthalten können;
  • Zeilen 11–17: Die Verbindungszeichenfolge zur verwendeten Datenbank. Diese hängt vom Element config[‘sgbd’] ab;
  • Zeilen 24–28: Die Entitäten der Anwendung, die einem Mapping unterzogen werden sollen ([sqlalchemy]). Wenn diese Zeilen ausgeführt werden, wurde der Python-Pfad bereits durch das Skript [config] eingerichtet;
  • Zeilen 30–40: das Mapping zwischen der Entität [Classe] und der Tabelle [classes];
  • Zeilen 30–35: Die Tabelle [classes] wird mit der Klasse [Table] aus [sqlalchemy] definiert. Wir geben an, dass diese Tabelle zwei Spalten hat:
    • die Spalte [id], die als Primärschlüssel dient und die Nummer der Klasse enthält, Zeile 33;
    • die Spalte [nom], die den Namen der Klasse enthält, Zeile 34;
  • Zeilen 31–32: Beachten Sie, dass die Syntax „x=y=z“ in Python zulässig ist: Der Wert von z wird y zugewiesen, anschließend der Wert von y an x;
  • Zeilen 37–40: Hier werden die Zuordnungen zwischen den Spalten der Tabelle [classes] und den Eigenschaften der Entität [Classe] aufgelistet;
  • Zeilen 42–57: Das Mapping zwischen der Entität [Elève] und der Tabelle [élèves];
  • Zeilen 51–57: Die Tabelle [élèves] wird mit der Klasse [Table] von [sqlalchemy] definiert. Wir geben an, dass diese Tabelle vier Spalten hat:
    • die Spalte [id], die als Primärschlüssel dient und die Nummer des Schülers enthält, Zeile 45;
    • die Spalte [nom], die den Nachnamen des Schülers enthält, Zeile 46;
    • die Spalte [prénom], die den Vornamen des Schülers enthält, Zeile 47. Beachten Sie, dass ein Spaltenname Zeichen mit Akzenten enthalten kann;
    • Zeile 49, die Spalte [classe_id], die die Nummer der Klasse enthält, zu der der Schüler gehört. Dies wird als Fremdschlüssel bezeichnet. [élèves.classe_id] ist ein Fremdschlüssel (ForeignKey) auf die Spalte [classes.id]. Das bedeutet, dass der Wert von [élèves.classe_id] in der Spalte [classes.id] vorhanden sein muss;
  • Zeilen 51–57: Hier werden die Zuordnungen zwischen den Spalten der Tabelle [élèves] und den Eigenschaften der Entität [Elève] aufgelistet:
    • Die Zeilen 53–55 sind leicht zu verstehen;
    • Zeile 56 ist schwieriger: Sie definiert den Wert der Eigenschaft [Elève.classe] als durch die Fremdschlüsselbeziehung berechnet, die die Tabellen [élèves] und [classes] miteinander verbindet. Die Parameter der Funktion [relationship] lauten wie folgt:
      • [Classe]: Dies ist der Name der Entität, mit der die Entität [Elève] eine Fremdschlüsselbeziehung unterhält. Diese Beziehung muss in der Tabelle [élèves] durch das Vorhandensein eines Fremdschlüssels auf die Tabelle [classes] zum Ausdruck kommen. Wir wissen, dass dieser vorhanden ist;
      • [backref="élèves"]: Der Name einer Eigenschaft, die der Entität [Classe] hinzugefügt wird. [Classe.élèves] ist die Liste aller Schüler der Klasse. Diese Eigenschaft darf noch nicht existieren. Falls sie bereits existiert, muss hier einfach ein anderer Name für [backref] gewählt werden. Der Entwickler muss sich nicht um diese Eigenschaft kümmern. Das übernimmt [sqlalchemy]. Er muss lediglich wissen, dass sie existiert – hinzugefügt von [sqlalchemy] – und dass er sie in seinem Code verwenden kann;
      • [lazy=’select’]: Das bedeutet, dass ORM nicht versuchen darf, der Eigenschaft [Elève.classe] sofort einen Wert zuzuweisen. Es darf ihren Wert erst dann ermitteln, wenn der Code dies explizit anfordert. Das heißt:
      • Wenn der Code die Liste aller Schüler abfragt, werden diese zurückgegeben, aber ihre Eigenschaft [classe] wird nicht berechnet;
      • etwas später interessiert sich der Code für einen bestimmten Schüler [e] und verweist auf dessen Klasse [e.classe]. Dieser Verweis zwingt [sqlalchemy] dann dazu, eine Datenbankabfrage durchzuführen, um die Klasse des Schülers abzurufen, und zwar für den Entwickler transparent;
      • außerdem soll durch die Einbindung von [lazy=’select’] unnötige Datenbankabfragen vermieden werden;
  • Zeile 56: Wenn ORM eine Zeile aus der Tabelle [élèves] abruft, erhält es die Informationen aus [id, nom, prénom, classe_id]. Auf dieser Grundlage muss es ein Objekt „Schüler“ (ID, Nachname, Vorname, Klasse) erstellen. Bei den Eigenschaften [id, nom, prénom] gibt es keine Schwierigkeiten. Bei der Eigenschaft [classe] ist es komplizierter. Ihr Wert ist eine Objektreferenz vom Typ [Classe]. Das Objekt „ORM“ enthält jedoch nur eine Information vom Typ „[élèves.classe_id]“. Da [élèves.classe_id] ein Fremdschlüssel auf die Spalte [classes.id] ist, wird hier angegeben, diese Beziehung zu nutzen, um aus der Tabelle [classes] die Zeile mit der ID=[élèves.classe_id] (die zwangsläufig existiert) abzurufen und aus dieser Zeile das Objekt [Classe] zu erstellen, das von der Eigenschaft [Elève.classe] erwartet wird;
  • Zeilen 59–71: das Mapping zwischen der Entität [Matière] und der Tabelle [matières];
  • Zeilen 59–65: Definition der Tabelle [sqlalchemy] mit dem Namen [matières];
  • Zeilen 66–71: Hier werden die Zuordnungen zwischen den Spalten der Tabelle [matières] und den Eigenschaften der Entität [Matière] aufgelistet. Hier gibt es keine Schwierigkeiten;
  • Zeilen 73–90: das Mapping zwischen der Entität [Note] und der Tabelle [notes];
  • Zeilen 73–82: Definition der Tabelle [sqlalchemy] mit dem Namen [notes]. Sie hat zwei Fremdschlüssel:
    • Zeile 79: Die Spalte [notes.élève_id] bezieht ihre Werte aus der Spalte [élèves.id]]. Dieser Fremdschlüssel verdeutlicht, dass eine Note die Note eines bestimmten Schülers ist;
    • Zeile 81: Die Spalte „[notes.matière_id]“ bezieht ihre Werte aus der Spalte „[matières.id]“. Dieser Fremdschlüssel verdeutlicht, dass eine Note eine Note in einem bestimmten Fach ist;
  • Zeilen 84–90: Die Zuordnung zwischen der Entität [Note] und der Tabelle [notes]:
    • Zeile 88: Die Eigenschaft [Note.élève] muss den Wert einer Instanz vom Typ [Elève] haben. Die Tabelle ORM enthält in der Zeile der Tabelle [notes] lediglich die Spalte [notes.élève_id], die auf die Spalte [élèves.id] verweist. Hier soll diese Fremdschlüsselbeziehung genutzt werden, um die Instanz [Elève] zu finden, deren Note vorliegt. Außerdem wird [relationship(Elève, backref="notes", …)] die neue Eigenschaft [Elève.notes] anlegen, die die Liste der Noten des Schülers enthält. Diese Eigenschaft darf in der Klasse [Elève] noch nicht vorhanden sein;
    • Zeile 89: Die Eigenschaft [Note.matière] muss als Wert eine Instanz vom Typ [Matière] haben. Die Tabelle ORM enthält in der Zeile der Tabelle [notes] lediglich die Spalte [notes.matière_id], die auf die Spalte [matières.id] verweist. Hier soll diese Fremdschlüsselbeziehung genutzt werden, um die Instanz [Matière] zu finden, für die eine Note vorliegt. Außerdem wird [relationship(Matière, backref="notes", …)] die neue Eigenschaft [Matière.notes] anlegen, die die Liste der Noten in dem Fach enthält. Diese Eigenschaft darf in der Klasse [Matière] noch nicht vorhanden sein;
  • Zeilen 92–96: Für jede von [BaseEntity] abgeleitete Entität wird die Liste der Eigenschaften definiert, die aus dem Eigenschaftswörterbuch der Entität (BaseEntity.asdict) ausgeschlossen werden sollen. Wir haben gesehen, dass [sqlalchemy] die Eigenschaft [_sa_instance_state] zu allen zugeordneten Entitäten hinzufügte. Diese soll nicht im Eigenschaftsverzeichnis enthalten sein. Außerdem haben wir gesehen, dass die vorherigen Zuordnungen den Entitäten neue Eigenschaften hinzugefügt hatten:
    • [Elève.notes]: alle Noten des Schülers;
    • [Classe.élèves]: alle Schüler der Klasse;
    • [Matière.notes]: alle Noten des Fachs;

Im Allgemeinen möchte man nicht, dass diese Eigenschaften in den Status der Entität aufgenommen werden. Denn die Berechnung ihres Werts ist mit einem Aufwand verbunden SQL und dieser Wert ist oft unnötig. Wenn man also den Schüler mit dem Namen „X“ abruft:

  • (Fortsetzung)
    • gibt ORM ein Entity [Elève(id, nom, prénom, classe, notes)] zurück. Aufgrund von [lazy=’select’] wurden die Eigenschaften [classe, notes], die mit Fremdschlüsseln der Datenbank verknüpft sind, nicht berechnet;
    • wenn ich nun die Zeichenfolge jSON dieses Schülers anzeige, wissen wir, dass es sich um die Zeichenfolge jSON aus dem Wörterbuch [asdict] der Entität handelt. Wenn die Eigenschaften [classe] und [notes] darin enthalten sind, muss [sqlalchemy] zwangsläufig Abfragen an SQL stellen, um deren Werte zu berechnen. Das ist aufwendig. Wenn sich diese Abfragen vermeiden lassen, ist das vorzuziehen;
    • hier haben wir alle Eigenschaften ausgeschlossen, die mit einem Fremdschlüssel verknüpft sind;
  • Zeilen 98–100: Instanziierung und Konfiguration eines [Session factory] (factory = Produktionsfabrik). Das Objekt [Session] dient dazu, transaktionsgebundene [sqlalchemy]-Sitzungen zu erstellen;
  • Zeilen 102–103: Erstellung einer SQLAlchemy-Sitzung;
  • Zeile 106: Bestimmte Elemente der Konfiguration [sqlalchemy] werden in das globale Konfigurationswörterbuch der Anwendung übernommen;
  • Zeile 109: Dieses Wörterbuch wird zurückgegeben;

Die Datei [config_layers] konfiguriert die Schichten der Anwendung:


def configure(config: dict) -> dict:
    # Instanziierung der Schicht [dao]
    from DatabaseDao import DatabaseDao
    dao = DatabaseDao(config)

    # Instanziierung der Schicht [métier]
    from Métier import Métier
    métier = Métier(dao)

    # Instanziierung der Schicht [ui]
    from Console import Console
    ui = Console(métier)

    # Die Schichten werden in die Konfiguration aufgenommen
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # Die Konfiguration wird zurückgegeben
    return config
  • Zeile 1: Die Funktion [configure] erhält das globale Konfigurationswörterbuch der Anwendung;
  • Zeilen 2–12: Die Ebenen der Anwendung werden instanziiert;
  • Zeilen 15–17: Die Referenzen der Schichten werden in die globale Konfiguration aufgenommen;
  • Zeile 20: Die neue Konfiguration wird zurückgegeben;

19.6.5. Die Schicht [dao] – 1

Image

Man muss hier verstehen, dass die Schicht [dao] [3] mit derORM [sqlalchemy] [4] kommuniziert, die wie im vorigen Absatz beschrieben konfiguriert wurde. Von den drei Schichten [ui, métier, dao] der Anwendung |troiscouches v01| muss nur die Schicht [dao] neu geschrieben werden. Die Schichten [ui, métier] bleiben erhalten.

Die Implementierung der Schicht [dao] wurde im Ordner [services] abgelegt:

Image

[InterfaceDatabaseDao] ist die Schnittstelle der Schicht [dao]:


from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao


class InterfaceDatabaseDao(InterfaceDao, ABC):

    # Initialisierung der Datenbank
    @abstractmethod
    def init_database(self, data: dict):
        pass
  • Zeile 6: Die Schnittstelle [InterfaceDatabaseDao] leitet sich sowohl von der Klasse [ABC] ab, um eine abstrakte Klasse zu sein, als auch von der Schnittstelle [InterfaceDao] des Projekts |troiscouches v01|;
  • Zeilen 8–11: Die Methode [init_database] wird zu den von [InterfaceDao] geerbten Methoden hinzugefügt. Ihre Aufgabe besteht darin, die Datenbank mit den Daten aus dem Wörterbuch [data] zu initialisieren, die ihr in Zeile 10 als Parameter übergeben werden;

Zur Erinnerung: Die Schnittstelle [InterfaceDao] sah wie folgt aus:


# Importe
from abc import ABC, abstractmethod

# DAO-Schnittstelle
from Elève import Elève


class InterfaceDao(ABC):
    # Liste der Klassen
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # Liste der Schüler
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # Fächerliste
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # Notenliste
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # Notenliste eines Schülers
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # einen Schüler anhand seiner ID suchen
    @abstractmethod
    def get_élève_by_id(self: object, élève_id: int) -> Elève:
        pass

Die Implementierung der Schicht [dao] lautet wie folgt:


from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # Datenbank = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

  • Zeile 11: Die Klasse [DatabaseDao] implementiert die Schnittstelle [InterfaceDatabaseDao];
  • Zeilen 13–16: Der Konstruktor der Klasse. Er erhält als Parameter das Wörterbuch mit der Anwendungskonfiguration;
  • Zeile 15: Die Konfiguration [sqlalchemy] wird gespeichert;
  • Zeile 16: Die Sitzung [sqlalchemy], über die die Datenbank bearbeitet wird, wird gespeichert;
  • Zeile 18: Die Methode [init_database] initialisiert die Datenbank mit dem Wörterbuch [data];

Das Dictionary [data] wird durch das folgende Skript [data.py] implementiert:


def configure():
    from Classe import Classe
    from Elève import Elève
    from Matière import Matière
    from Note import Note

    # Die Klassen werden instanziiert
    classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
    classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
    classes = [classe1, classe2]
    # die Fächer
    matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
    matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
    matières = [matière1, matière2]
    # die Schüler
    élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
    élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
    élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
    élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
    élèves = [élève11, élève21, élève32, élève42]
    # die Noten der Schüler in den verschiedenen Fächern
    note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
    note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
    note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
    note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
    note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
    note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
    note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
    note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
    notes = [note1, note2, note3, note4, note5, note6, note7, note8]
    # das Ganze wird zusammengefasst
    data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
    # die Daten werden bereitgestellt
    return data
  • Zeile 34: Das Dictionary, das an die Methode [init_database] übergeben wird. Dieses Dictionary besteht aus den folgenden Schlüsseln (Zeile 32):
    • [élèves]: die Liste der Schüler;
    • [classes]: die Klassenliste;
    • [matières]: die Liste der Fächer;
    • [notes]: die Liste der Noten aller Schüler in allen Fächern;

Kommen wir zurück zur Methode [init_database]:


def init_database(self, data: dict):
        # Datenbankkonfiguration
        database = self.database
        engine = database['engine']
        metadata = database['metadata']
        tables = database['tables']

        try:
            # Löschen der vorhandenen Tabellen
            # checkfirst=True: Prüft zunächst, ob die Tabelle existiert
            tables["notes"].drop(engine, checkfirst=True)
            tables["matières"].drop(engine, checkfirst=True)
            tables["élèves"].drop(engine, checkfirst=True)
            tables["classes"].drop(engine, checkfirst=True)

            # Neuerstellung der Tabellen anhand des Mappings
            metadata.create_all(engine)

            # Tabellen befüllen
            session = self.session

            # Klassen
            classes = data["classes"]
            for classe in classes:
                session.add(classe)

            # Fächer
            matières = data["matières"]
            for matière in matières:
                session.add(matière)

            # Schüler
            élèves = data["élèves"]
            for élève in élèves:
                session.add(élève)

            # Noten
            notes = data["notes"]
            for note in notes:
                session.add(note)

            # Commit
            session.commit()
        except (DatabaseError, InterfaceError, IntegrityError) as erreur:
            # Abbruch der Sitzung
            if session:
                session.rollback()
            # Ausnahme wird weitergeleitet
            raise MyException(23, f"{erreur}")
  • Zeilen 3–6: Hier werden Informationen aus der Datenbankkonfiguration abgerufen;
  • Zeilen 9–14: Wir haben gesehen, dass die Konfiguration [sqlalchemy] vier Entitäten vier Tabellen [élèves, matières, classes, notes] zugeordnet hatte. Zunächst werden diese Tabellen gelöscht, sofern sie vorhanden sind;
  • Zeilen 16–17: Die vier soeben gelöschten Tabellen werden neu angelegt;
  • Zeilen 22–25: Wir nehmen alle Klassen in die Sitzung auf;
  • Zeilen 27–30: Alle Fächer werden in die Sitzung aufgenommen;
  • Zeilen 32–35: Alle Schüler werden in die Sitzung aufgenommen;
  • Zeilen 37–40: Alle Noten werden in die Sitzung aufgenommen;
  • Bei diesen Hinzufügungen haben wir eine bestimmte Reihenfolge befolgt. Wir haben mit den Entitäten begonnen, die keine Beziehungen zu anderen Entitäten haben, und mit denen abgeschlossen, die solche Beziehungen aufweisen. Wenn wir also die Schüler in die Sitzung aufnehmen, befinden sich die Klassen, auf die diese verweisen, bereits in der Sitzung;
  • Zeile 43: Die Sitzung [sqlalchemy] wurde bestätigt. Nach diesem Vorgang kann man sicher sein, dass alle Daten der Sitzung mit der Datenbank synchronisiert wurden. Mit anderen Worten: Sie sind in den Tabellen angekommen. Dies war dank der Zuordnungen möglich, die in der Konfiguration von [sqlalchemy] vorgenommen wurden. [sqlalchemy] weiß, wie jede Entität in den Tabellen gespeichert werden muss. [sqlalchemy] hat außerdem die Fremdschlüssel generiert, die die Tabellen enthalten können;
  • Zeilen 44–49: Tritt ein Problem auf, wird die Sitzung von [sqlalchemy] abgebrochen und in Zeile 49 wird eine Ausnahme ausgelöst;

19.6.6. Initialisierung der Datenbank

Image

Das Skript [main_init_database] initialisiert die Datenbank mit dem Inhalt des Skripts [data.py]. Sein Code lautet wie folgt:


# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})

# Der Syspath ist konfiguriert – die Importe können durchgeführt werden
from MyException import MyException

# Die in die Datenbank einzufügenden Daten werden abgerufen
import data
data = data.configure()

# Die Ebene wird abgerufen [dao]
dao = config["dao"]

# ----------- main
try:
    # Erstellung und Initialisierung der Datenbanktabellen
    dao.init_database(data)
except MyException as ex:
    # Der Fehler wird angezeigt
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # Freigabe der von der Anwendung belegten Ressourcen
    import shutdown
    shutdown.execute(config)
# Ende
print("Travail terminé...")
  • Zeilen 1–11: Das Skript erwartet einen Parameter [mysql] oder [pgres], je nachdem, ob eine Datenbank MySQL oder PostgreSQL initialisiert werden soll;
  • Zeilen 13–15: Die Anwendung ist für den als Parameter übergebenen Wert SGBD konfiguriert;
  • Zeilen 20–22: Die Daten, die in die Datenbank eingefügt werden sollen, werden abgerufen;
  • Zeile 25: Die Ebene [dao] wurde bereits instanziiert und ist in der Anwendungskonfiguration verfügbar;
  • Zeile 30: Die Datenbank wird initialisiert;
  • Zeilen 34–37: Unabhängig davon, ob ein Fehler aufgetreten ist oder nicht, werden die Ressourcen der Anwendung mithilfe des Moduls [shutdown] freigegeben;

Das Modul [shutdown.py] lautet wie folgt:


def execute(config: dict):
    # Freigabe der von der Anwendung belegten Ressourcen
    sqlalchemy_session = config['database']['session']
    if sqlalchemy_session:
        sqlalchemy_session.close()

Die Funktion [shutdown.execute] schließt die Sitzung [sqlalchemy], die zur Initialisierung der Datenbank verwendet wurde.

Wir erstellen eine erste Ausführungskonfiguration (siehe |Ausführungskonfiguration|), um [main_init_database] zusammen mit SGBD und MySQL auszuführen:

Image

Die Ergebnisse der Ausführung dieser Konfiguration lauten in phpMyAdmin wie folgt:

Image

Image

Image

Für SGBD und [PostgreSQL] verwenden wir die folgende Ausführungskonfiguration:

Image

Bei der Ausführung lauten die Ergebnisse in [pgAdmin] wie folgt:

Image

Image

Image

Es ist zu beachten, wie einfach die Änderung von SGBD vorgenommen werden konnte.

19.6.7. Die Schicht [dao] – 2

Wir kehren zur Klasse [DatabaseDao] zurück, die die Schicht [dao] implementiert. Bislang haben wir nur die Implementierung der Methode [init_database] gezeigt. Nun zeigen wir die Implementierung der anderen Methoden:


from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

    # Liste aller Klassen
    def get_classes(self: object) -> list:
        # Abfrage
        return self.session.query(Classe).all()

    # Liste aller Schüler
    def get_élèves(self: object) -> list:
        # Abfrage
        return self.session.query(Elève).all()

    # Liste aller Fächer
    def get_matières(self: object) -> list:
        # Abfrage
        return self.session.query(Matière).all()

    # Liste der Noten aller Schüler
    def get_notes(self: object) -> list:
        # Abfrage
        return self.session.query(Note).all()

    # Liste der Noten eines bestimmten Schülers
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # Der Schüler wird gesucht – es wird eine Ausnahme ausgelöst, wenn er nicht existiert
        # die Abfrage wird zurückgegeben
        élève = self.get_élève_by_id(élève_id)
        # seine Noten werden abgerufen (Lazy Loading)
        notes = élève.notes
        # ein Wörterbuch wird zurückgegeben
        return {"élève": élève, "notes": notes}

    # Ein Schüler wird anhand seiner Nummer identifiziert
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # man sucht den Schüler
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # Wurde er gefunden?
        if élèves:
            return élèves[0]
        else:
            raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")

    # Ein Schüler, der anhand seines Namens identifiziert wurde
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # Der Schüler wird gesucht
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # – wurde er gefunden?
        if élèves:
            return élèves[0]
        else:
            raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")

    # Eine Klasse anhand ihrer Nummer ausfindig gemacht
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # Die Klasse wird gesucht
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # Hat man sie gefunden?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # eine Klasse, die durch ihren Namen gekennzeichnet ist
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # Wir suchen die Klasse
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # – wurde sie gefunden?
        if classes:
            return classes[0]
        else:
            raise MyException(14, f"La classe de nom {classe_name} n'existe pas")

    # ein Fach, das anhand seiner Nummer identifiziert wurde
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # Man sucht das Fach
        matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
        # Haben wir es gefunden?
        if matières:
            return matières[0]
        else:
            raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")

    # ein Stoff, der anhand seines Namens identifiziert wurde
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # Wir suchen den Stoff
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # – wurde es gefunden?
        if matières:
            return matières[0]
        else:
            raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
  • Zeilen 21–24: Die Methode [get_classes] soll die Liste der Klassen der Schule zurückgeben. In Zeile 20 verwenden wir eine bereits bekannte Abfrage;
  • Zeilen 26–39: drei weitere ähnliche Methoden, um die Listen der Schüler, der Fächer und der Noten abzurufen;
  • Zeilen 51–59: Die Methode [get_élève_by_id] soll einen durch seine Nummer identifizierten Schüler zurückgeben. Sie löst eine Ausnahme aus, wenn dieser nicht existiert;
    • Zeile 54: Hier wird eine gefilterte Abfrage verwendet. Man erhält entweder eine leere Liste oder eine Liste mit einem Element;
    • Zeile 57: Ist die abgefragte Liste nicht leer, wird das erste Element der Liste zurückgegeben;
    • ansonsten wird in Zeile 59 eine Ausnahme ausgelöst;
  • Zeilen 41–49: Die Methode [get_notes_for_élève_by_id] muss die Noten eines durch seine Nummer identifizierten Schülers zurückgeben:
    • Zeile 45: Es wird die Methode [get_élève_by_id] verwendet, um die Entität „Schüler“ des Schülers abzurufen;
    • Zeile 47: Es wird die Eigenschaft [Elève.notes] verwendet, die durch das Mapping zwischen der Entität [Note] und der Tabelle [notes] erstellt wurde (siehe Abschnitt |SQLAlchemy-Konfiguration|) und die die Noten des Schülers darstellt;
    • Zeile 49: Es wird ein Dictionaries zurückgegeben;
  • Zeilen 61–109: eine Reihe ähnlicher Methoden, die es ermöglichen:
    • einen Schüler anhand seines Namens suchen, Zeilen 61–69;
    • eine Klasse suchen, Zeilen 71–89;
    • ein Fach suchen, Zeilen 91–109;

19.6.8. Das Skript [main_joined_queries]

Image

Das Skript [main_joined_queries] trägt diesen Namen, weil es die impliziten Abfragen von [sqlalchemy] sichtbar machen soll, mit denen Informationen aus mehreren Tabellen abgerufen werden. Diese für den Programmierer verborgenen Abfragen werden jedes Mal ausgeführt, wenn eine Eigenschaft einer Entität im Mapping der Entität der Funktion [relationship] zugeordnet wurde. Zum Beispiel:


    # Zuordnung
    mapper(Note, tables['notes'], properties={
        'ID: notes_table.c.id,
        'Wert: notes_table.c.valeur,
        'Schüler': Beziehung(Schüler, backref="Noten", lazy="select"),
        '„Fach“: Beziehung(Fach, backref="Noten", lazy="select")
    })

Oben ist das Mapping zwischen der Entität [Note] und der Tabelle [notes] dargestellt:

  • Zeile 5: Wenn die Eigenschaft [élève] einer Entität [Note] zum ersten Mal abgefragt wird, wird sie über eine Abfrage SQL in der Tabelle [élèves] gesucht. Solange diese Eigenschaft nicht abgefragt wurde, bleibt sie undefiniert (Lazy Load). Sobald sie abgerufen wurde, bleibt ihr Wert im Speicher der Entität ORM erhalten. Wird sie ein zweites Mal aufgerufen, liefert die ORM ihren Wert sofort, ohne dass eine neue Abfrage SQL erforderlich ist. All dies ist für den Entwickler transparent;
  • das Gleiche gilt für die inverse Eigenschaft [Elève.notes] (backref), Zeile 5;
  • das Gleiche gilt für die Eigenschaft [Note.matière] und ihre inverse Eigenschaft [Matière.notes] (Backref), Zeile 6;

Das Skript [main_joined_queries] lautet wie folgt:


# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({"sgbd": sgbd})

# Der Syspath ist konfiguriert – die Importe können durchgeführt werden
from MyException import MyException

# die Schicht [dao]
dao = config["dao"]
try:
    # Schüler nach ID
    print("élève id=11 -----------")
    élève = dao.get_élève_by_id(11)
    print(f"élève={élève}")
    # die Klasse des Schülers (Lazy Loading)
    classe = élève.classe
    print(f"classe de l'élève : {classe}")
    # Schüler derselben Klasse (Lazy Loading)
    print("élèves dans la même classe :")
    for élève in classe.élèves:
        print(f"élève={élève}")

    # ein Schüler nach seinem Namen
    print("élève nom='nom2' -----------")
    print(f"élève={dao.get_élève_by_name('nom2')}")
    # seine Klasse (Lazy Loading)
    print(f"classe de l'élève : {élève.classe}")

    # Noten eines Schülers
    print("notes de l'élève id=11 -----------")
    # zuerst der Schüler
    élève = dao.get_élève_by_id(11)
    # dann seine Noten (Lazy Loading)
    for note in élève.notes:
        # die Note
        print(f"note={note}, "
              # das Fach der Note (Lazy Loading)
              f"matière={note.matière}")

    # die Schüler einer Klasse
    print("élèves de la classe nom='classe1' -----------")
    # zuerst die Klasse
    classe = dao.get_classe_by_name('classe1')
    # dann die Schüler (Lazy Loading)
    for élève in classe.élèves:
        print(élève)

    # das Gleiche gilt für [classe2]
    print("élèves de la classe de nom 'classe2' -----------")
    classe = dao.get_classe_by_name('classe2')
    for élève in classe.élèves:
        print(élève)

    # die Noten in einem Fach
    print("matière de nom='matière1' -----------")
    # zuerst das Fach
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # dann die Noten in diesem Fach (Lazy Loading)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # das Gleiche gilt für Fach 2
    print("matière de nom='matière2' -----------")
    matière = dao.get_matière_by_name('matière2')
    print(f"matière={matière}")
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(f"note={note}")
except MyException as ex1:
    # Der Fehler wird angezeigt
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # Der Fehler wird angezeigt
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # Ressourcen werden freigegeben
    import shutdown
    shutdown.execute(config)

Die Kommentare reichen aus, um den Code zu verstehen.

Wir erstellen eine Ausführungskonfiguration für MySQL:

Image

Die Ergebnisse der Ausführung lauten wie folgt:

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/sqlalchemy/05/main/main_joined_queries.py mysql
élève id=11 -----------
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
classe de l'élève : {"nom": "classe1", "id": 1}
élèves dans la même classe :
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élève nom='nom2' -----------
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
classe de l'élève : {"nom": "classe1", "id": 1}
notes de l'élève id=11 -----------
note={"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}, matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}, matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
élèves de la classe nom='classe1' -----------
{"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
{"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élèves de la classe de nom 'classe2' -----------
{"classe_id": 2, "nom": "nom3", "prénom": "prénom3", "id": 32}
{"classe_id": 2, "nom": "nom4", "prénom": "prénom4", "id": 42}
matière de nom='matière1' -----------
matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
Notes dans la matière : 
{"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}
{"matière_id": 1, "valeur": 12.0, "élève_id": 21, "id": 2}
{"matière_id": 1, "valeur": 14.0, "élève_id": 32, "id": 3}
{"matière_id": 1, "valeur": 16.0, "élève_id": 42, "id": 4}
matière de nom='matière2' -----------
matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
Notes dans la matière : 
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}
note={"matière_id": 2, "valeur": 8.0, "élève_id": 21, "id": 6}
note={"matière_id": 2, "valeur": 10.0, "élève_id": 32, "id": 7}
note={"matière_id": 2, "valeur": 12.0, "élève_id": 42, "id": 8}

Process finished with exit code 0

Um diese Ergebnisse zu verstehen, muss man bedenken, dass bestimmte Eigenschaften aus dem Entitätswörterbuch ausgeschlossen wurden (siehe |Konfiguration|):


    # Konfiguration der Entitäten [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

Wenn man also in Zeile 26 des Codes „[print(f"élève={élève}")]“ eingibt, besagt Zeile 1 oben, dass die Eigenschaften von „['_sa_instance_state', 'notes', 'classe']“ nicht angezeigt werden. Dies ist in Zeile 3 der Ergebnisse zu sehen. Alle anderen Eigenschaften werden angezeigt. So entdeckt man ebenfalls in Zeile 3 eine neue Eigenschaft [classe_id], die ursprünglich in der Entität [Elève] nicht vorhanden war. Diese Eigenschaft entspricht direkt der Spalte [classe_id] der Tabelle [élèves]. Somit hat [sqlalchemy] der Entität [Elève] die folgenden Eigenschaften hinzugefügt: [classe_id, _sa_instance_state, notes]. Dies muss man beachten, insbesondere weil diese Eigenschaften in der zugeordneten Entität noch nicht vorhanden sein dürfen.

Die aus dem Entitätswörterbuch ausgeschlossenen Eigenschaften sind wichtig. Wenn beispielsweise die Eigenschaften [notes, élève] der Entität [Elève] nicht ausgeschlossen werden, dann zeigt die Operation [print(f"élève={élève}")] diese an und löst somit, wie gerade erläutert, implizite Abfragen SQL (Lazy Loading) auslösen, um die Werte dieser Eigenschaften abzurufen. Wenn, wie hier, eine Liste von Schülern angezeigt wird, werden die impliziten Operationen SQL für jeden Schüler durchgeführt. Dies kann einerseits unnötig und andererseits sicherlich mit hohem Zeitaufwand verbunden sein.

Um das Skript mit einer PostgreSQL-Datenbank auszuführen, erstellen wir die folgende Ausführungskonfiguration:

Image

Die Ausführung liefert dieselben Ergebnisse wie mit MySQL.

19.6.9. Das Skript [main_stats_for_élève]

Image

Das Skript [main_stats_for_élève] ist dasjenige, das bereits in der Anwendung [|troiscouches v01] verwendet wurde. Damals hieß es noch [main]. Es handelt sich um eine Konsolenanwendung, mit der bestimmte Kennzahlen zu den Noten eines Schülers abgerufen werden können: [moyenne pondérée, min, max, liste]. Sie fügt sich in die folgende Architektur ein:

Image

In dieser Schichtenarchitektur wurde zwischen der Anwendung |troiscouches v01| und dieser Anwendung lediglich die Schicht [dao] geändert. Da die neue Schicht [dao] die Schnittstelle [InterfaceDao] der alten Schicht [dao] einhält, müssen die Schichten [ui, métier] nicht geändert werden. Man kann also weiterhin die in der Anwendung |troiscouches v01| definierten Schichten verwenden.

Das Skript [main_stats_for_élève] implementiert die Ebene [main] aus dem obigen Schema wie folgt:


# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})

# Der Syspath ist konfiguriert – die Importe können durchgeführt werden
from MyException import MyException

# die Schicht [ui]
ui = config["ui"]
try:
    # Ausführung der Schicht [ui]
    ui.run()
except MyException as ex1:
    # Der Fehler wird angezeigt
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # Der Fehler wird angezeigt
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # Ressourcen werden freigegeben
    import shutdown
    shutdown.execute(config)
  • Zeile 20: Es wird eine Referenz auf die Schicht [ui] in der Anwendungskonfiguration abgerufen;
  • Zeile 24: Der Dialog mit dem Benutzer wird mithilfe der einzigen Methode der Schicht [ui] gestartet;

Eine Ausführungskonfiguration für PostgreSQL würde wie folgt aussehen:

Image

Hier ein Ausführungsbeispiel mit dieser Konfiguration:


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/sqlalchemy/05/main/main_stats_for_élève.py pgres
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"prénom": "prénom1", "id": 11, "classe_id": 1, "nom": "nom1"}, notes=[10.0 6.0], max=10.0, min=6.0, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[11, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *

Process finished with exit code 0