Skip to content

19. Wykorzystanie skryptu ORM SQLALCHEMY

W poprzednim rozdziale pokazano, że w niektórych przypadkach można napisać kod niezależny od SGBD, stosowany w następującej architekturze:

Image

W niniejszym rozdziale będziemy korzystać z ORM (mapera obiektowo-relacyjnego) [sqlalchemy], aby uzyskać jednolity dostęp do SGBD niezależnie od używanego SGBD. ORM umożliwia dwie rzeczy:

  • umożliwia skryptowi komunikację z SGBD bez wysyłania poleceń SQL;
  • ukrywa przed skryptem specyfikę poszczególnych SGBD;

Architektura wygląda teraz następująco:

Skrypt jest teraz oddzielony od łączników przez ORM. Komunikuje się z ORM za pomocą klas i metod. Nie wykonuje kodu SQL. To moduł ORM wykonuje te operacje za pomocą łączników, z którymi jest połączony. Ukrywa on przed skryptem specyfikę tych łączników. Dzięki temu kod skryptu jest odporny na zmianę łącznika (a więc modułu SGBD);

Struktura analizowanych skryptów będzie wyglądać następująco:

Image

19.1. Instalacja modułu ORM [sqlalchemy]

ORM [sqlalchemy] ma postać pakietu w języku Python, który należy zainstalować w terminalu Python:


(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. Skrypty 01: podstawy

Image

  • w [1] znajdują się skrypty, które będziemy analizować. Skrypty te będą korzystać z klas z [2]: BaseEntity, MyException, Personne, Utils;

19.2.1. Konfiguracja

Plik [config] konfiguruje aplikację w następujący sposób:


def configure():
    # root_dir
    # ścieżka bezwzględna odnosząca się do ścieżek względnych konfiguracji
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # ścieżki bezwzględne zależności
    absolute_dependencies = [
        # BaseEntity, MyException, Osoba, Narzędzia
        f"{root_dir}/classes/02/entities",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # konfiguracja klas
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # przekazujemy konfigurację
    return {}

Komentarze

  • wiersz 8: dodajemy do ścieżki Python folder zawierający klasy [BaseEntity, MyException, Personne, Utils];
  • wiersze 12–13: ustala się ścieżkę Python Path aplikacji;
  • wiersze 16–17: warto pamiętać, że klasa |BaseEntity| posiada atrybut klasy o nazwie [excluded_keys]. Atrybut ten jest listą, do której umieszcza się właściwości klasy, których nie chcemy, aby pojawiały się w jej słowniku (funkcja asdict). W tym przypadku wykluczamy właściwość [_sa_instance_state] ze stanu klasy [Personne]. Wkrótce zobaczymy, dlaczego;

19.2.2. Skrypt [démo]

Skrypt [démo] przedstawia pierwsze zastosowanie funkcji ORM i [sqlalchemy]:


# pobieramy konfigurację aplikacji
import config

config = config.configure()

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

from Personne import Personne

# metadane
metadata = MetaData()

# tabela
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')
                        )
# klasa „Osoba” przed mapowaniem
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")

# mapowanie
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'imię: personnes_table.c.prenom,
    'nazwisko: personnes_table.c.nom,
    'wiek: personnes_table.c.age
})

# osoba1 nie została zmodyfikowana
print(f"personne1={personne1.__dict__}")
# klasa „Osoba” została zmodyfikowana – została „wzbogacona”
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

Komentarze

  • wiersze 1–4: konfiguracja aplikacji;
  • wiersze 6–10: importuje się moduły niezbędne do działania skryptu;
  • wiersz 13: [MetaData] jest klasą klasy [sqlalchemy];
  • wiersze 15–22: [Table] jest klasą klasy [sqlalchemy]. Służy ona do opisania tabeli w bazie danych. W tym miejscu opiszemy tabelę [personnes] z bazy MySQL [dbpersonnes] omówionej w rozdziale |MySQL|;
    • wiersz 16: pierwszy parametr [personnes] to nazwa opisywanej tabeli;
    • wiersz 16: drugi parametr [metadata] to instancja [MetaData] utworzona w wierszu 13;
    • wiersze 17–22: każdy z kolejnych parametrów opisuje kolumnę tabeli przy użyciu składni właściwej dla klasy [sqlalchemy], ale zbliżonej do składni klasy SQL;
    • każda kolumna jest opisana za pomocą instancji klasy [Column] pochodzącej z klasy [sqlalchemy];
      • pierwszym parametrem jest nazwa kolumny;
      • drugim parametrem jest jej typ;
      • kolejne parametry to parametry nazwane:
        • wiersz 17: [primary_key=True], aby wskazać, że kolumna [id] jest kluczem głównym tabeli [personnes];
        • wiersz 18: [nullable=False], co oznacza, że kolumna ta musi koniecznie zawierać wartość przy wstawianiu wiersza do tabeli;
    • wiersz 21: wreszcie klasa [UniqueConstraint] pozwala opisać ograniczenie unikalności. W tym przypadku określa się, że kolumny (nazwisko, imię) muszą być unikalne w tabeli. Właściwość o nazwie [name] pozwala nadać nazwę temu ograniczeniu. Należy tu rozróżnić dwa przypadki:
      • opisujemy istniejącą tabelę. Należy wówczas wyszukać nazwę ograniczenia we właściwościach tabeli (phpMyAdmin lub pgAdmin);
      • opisujemy tabelę, którą zamierzamy utworzyć. W takim przypadku wpisujemy dowolną nazwę;
  • wiersze 23–25: tworzymy osobę o nazwie [personne1] i wyświetlamy jej słownik o nazwie [__dict__]. Otrzymamy tutaj:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • wiersze 27–33: wykonujemy mapowanie, tzn. tworzymy powiązanie między klasą [Personne] a tabelą [personnes]. Zasadniczo jest to powiązanie [propriétés de la classe  colonnes de la table]. Funkcja [mapper] przyjmuje tutaj trzy parametry:
    • wiersz 28: pierwszym parametrem jest nazwa klasy, dla której tworzone jest mapowanie;
    • wiersz 28: drugim parametrem jest tabela, z którą zostanie ona powiązana. Jest to obiekt [Table] utworzony w wierszu 16;
    • wiersz 28: trzecim parametrem jest tutaj parametr o nazwie [properties]. Jest to słownik, w którym klucze stanowią właściwości mapowanej klasy, a wartości – kolumny mapowanej tabeli. Aby wskazać kolumnę X tabeli [personnes_table], wpisuje się [personnes_table.c.X];
  • wiersze 35–36: po zakończeniu mapowania ponownie wyświetla się osoba [personne1]. Widzimy, że nie uległa ona zmianie:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • wiersze 37–39: tworzymy nową osobę o nazwie [personne2] i wyświetlamy ją. Otrzymujemy wówczas następujący wynik:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}

Widać, że słownik [__dict__] został gruntownie zmodyfikowany:

  • (ciąg dalszy)
    • pojawia się nowa właściwość [_sa_instance_state]. Widać, że jest to obiekt należący do klasy ORM [sqlalchemy];
    • pozostałe właściwości zostały pozbawione przedrostka wskazującego, do której klasy należały;

Można zatem stwierdzić, że operacja mapowania w wierszach 27–33 zmieniła klasę [Personne].

Gdy chcemy wyświetlić stan obiektu [Personne], zazwyczaj nie potrzebujemy właściwości [_sa_instance_state]. Służy ona bowiem wyłącznie do wewnętrznych celów obiektu [sqlalchemy] i zazwyczaj nas nie interesuje. Dlatego w skrypcie [config] zapisaliśmy:


    # konfiguracja klas
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

19.2.3. Skrypt [main]

Skrypt [main] będzie przetwarzał tabelę [personnes] z bazy MySQL [dbpersonnes], współpracując z tabelą [sqlalchemy]. Aby zrozumieć dalszy przebieg, należy przypomnieć sobie zastosowaną tutaj architekturę:

Image

Jeśli [Database1] jest bazą [dbpersonnes], widać, że połączenie między skryptem a tą bazą przebiega przez dwa elementy:

  • łącznik Python do SGBD MySQL;
  • SGBD i MySQL;

Skrypt [main] będzie komunikował się z ORM, który następnie będzie komunikował się z łącznikiem Python. Skrypt ORM komunikuje się z tym łącznikiem za pomocą narzędzi opisanych w akapitach dotyczących skryptów |MySQL| i |PostgreSQL|, w szczególności wysyłając polecenia SQL. Skrypt [main] nie będzie korzystał z poleceń SQL. Będzie on opierał się na API (interfejsie programowania aplikacji) dla ORM, składającym się z klas i interfejsów.

Skrypt [main] wygląda następująco:


# konfigurujemy aplikację
import config

config = config.configure()

# importy
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

# ciąg połączenia z bazą danych MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

# metadane
metadata = MetaData()

# tabela
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')
                        )

# mapowanie
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'imię: personnes_table.c.prenom,
    'nazwisko: personnes_table.c.nom,
    'wiek: personnes_table.c.age
})

# fabryka sesji
Session = sessionmaker()
Session.configure(bind=engine)

session = None
try:
    # sesja
    session = Session()

    # usunięcie tabeli [personnes]
    session.execute("drop table if exists personnes")

    # ponowne utworzenie tabeli na podstawie mapowania
    metadata.create_all(engine)

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

    # zapytanie
    personnes = session.query(Personne).all()

    # wyświetlenie
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # dwa kolejne wstawienia, z których drugie kończy się niepowodzeniem z powodu unikalności (imię, nazwisko)
    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}))

    # zapytanie
    personnes = session.query(Personne).all()

    # wyświetlenie
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # weryfikacja sesji
    session.commit()

except (InterfaceError, IntegrityError) as erreur:
    # wyświetlenie
    print(f"L'erreur suivante s'est produite : {erreur}")
    # anulowanie ostatniej sesji
    if session:
        print("rollback...")
        session.rollback()
finally:
    # zwolnienie zasobów sesji
    if session:
        session.close()

Komentarze

  • wiersze 1–4: konfiguracja aplikacji;
  • wiersze 7–9: importowana jest cała seria klas i interfejsów z biblioteki [sqlalchemy];
  • wiersz 11: importowana jest klasa [Personne];
  • wiersz 14: ciąg połączenia z bazą danych. Określa on:
    • używaną bibliotekę SGBD (mysql);
    • używany łącznik Python (mysql.connector bez kropki);
    • użytkownika, który się loguje (admpersonnes);
    • jego hasło (nobody);
    • komputer, na którym znajduje się SGBD (localhost = komputer, na którym znajduje się uruchamiany skrypt);
    • nazwa bazy danych (dbpersonnes);

Dzięki tym informacjom skrypt [sqlalchemy] może połączyć się z bazą danych. Należy pamiętać, że używany łącznik Python musi być już zainstalowany. Skrypt [sqlalchemy] tego nie robi.

  • wiersze 19–26: opis tabeli [personnes];
  • wiersze 28–34: mapowanie między klasą [Personne] a tabelą [personnes];
  • wiersze 36–38: większość operacji klasy [sqlalchemy] odbywa się w ramach sesji. Pojęcie sesji [sqlalchemy] jest zbliżone do pojęcia transakcji SQL. Sesje są tworzone na podstawie klasy [Session] zwracanej przez funkcję [sessionmaker] z wiersza 37;
  • wiersz 38: klasa [Session] jest powiązana z bazą [dbpersonnes] za pośrednictwem łańcucha połączenia z wiersza 14;
  • wiersz 43: tworzona jest sesja. Jak już wspomniano, sesję można porównać do transakcji;
  • wiersze 45–46: metoda [Session.execute] umożliwia wykonanie zlecenia SQL. Nie jest to częsta sytuacja, ponieważ wspomniano, że metoda ORM pozwala uniknąć użycia języka SQL;
  • wiersze 48–49: metoda [metadata.create_all] pozwala utworzyć wszystkie tabele korzystające z instancji [MetaData] z wiersza 17. Mamy tylko jedną: tabelę [personnes] zdefiniowaną w wierszach 20–26. [sqlalchemy] wykorzysta informacje z tych wierszy do utworzenia tabeli. Jest to pierwsza zaleta ORM: ukrywa specyfikę poszczególnych SGBD. Rzeczywiście, kolejność SQL i [create] może się znacznie różnić w zależności od konkretnego SGBD ze względu na typy danych przypisane do kolumn. Nie dokonano ujednolicenia typów danych. W związku z tym kolejność różni się w poszczególnych przypadkach. Tutaj, dzięki:
    • opisujemy w jednoznaczny sposób pożądaną tabelę;
    • [sqlalchemy] generuje odpowiedni obiekt [create] dla obiektu SGBD, który ma przed sobą;
  • wiersz 52: dodajemy obiekt [Personne] do sesji. Nie powoduje to automatycznego dodania go do bazy danych. ORM kieruje się bowiem własnymi zasadami synchronizacji z bazą danych. Zawsze stara się zoptymalizować liczbę wysyłanych zapytań. Weźmy przykład. Skrypt dodaje (add) dwie osoby (osoba1, osoba2) do sesji, a następnie wysyła zapytanie: chce wyświetlić wszystkie osoby znajdujące się w tabeli. [sqlalchemy] może postąpić w następujący sposób:
    • dodanie [personne1] może odbywać się w pamięci. Na razie nie ma potrzeby umieszczania go w bazie danych;
    • to samo dotyczy [personne2];
    • następnie pojawia się zapytanie typu [select]. Należy wówczas pobrać wszystkie wiersze z tabeli [personnes]. [sqlalchemy] umieści wówczas [personne1, personne2] w bazie danych, a następnie wykona zapytanie;

[sqlalchemy] przeprowadzi w ten sposób optymalizacje niewidoczne dla programisty.

  • wiersz 56: aby wykonać zapytanie typu [select] (chcę zobaczyć…), używa się metody [Session.query]. Parametrem metody [query] jest klasa przypisana do tabeli, do której kierowane jest zapytanie. Metoda ta zwraca typ [Query]. Metoda [Query.all] pobiera wszystkie obiekty typu [Personne] z sesji. Zwraca ona wszystkie wiersze z tabeli [personnes], każdy w postaci obiektu typu [Personne]. W tym celu [sqlalchemy] wykorzystuje mapowanie utworzone między klasą [Personne] a tabelą [personnes]. Wynikiem wiersza 56 jest lista obiektów [Personne];
  • wiersze 58–61: wyświetlane są elementy listy [personnes]. Ponieważ klasa [Personne] wywodzi się z klasy [BaseEntity], metoda [Personne.__str__], użyta tutaj niejawnie w wierszu 61, jest w rzeczywistości metodą [BaseEntity.__str__], która zwraca ciąg znaków jSON obiektu wywołującego. Ciąg ten jest ciągiem jSON ze słownika [Personne.asdict] (por. |BaseEntity|). Stwierdziliśmy, że po mapowaniu w każdym obiekcie [Personne] znajdziemy właściwość [_sa_instance_state]. Jednak wartość tej właściwości nie jest typu [BaseEntity]. Należy zatem wykluczyć ją ze słownika klasy [Personne], w przeciwnym razie wyświetlanie „zawiesza się”. Tak właśnie zrobiono w skrypcie [config];
  • wiersze 63–65: dodajemy dwie kolejne osoby o tym samym imieniu i nazwisku. Mamy jednak ograniczenie unikalności na połączeniu tych dwóch kolumn. Powinien więc wystąpić błąd. Właśnie to chcemy sprawdzić;
  • wiersze 67–68: ponownie żądamy listy wszystkich osób z bazy;
  • wiersze 70–73: i wyświetlamy je;
  • wiersze 75–76: sesja zostaje zatwierdzona („commit”). Jak sama nazwa wskazuje, transakcja leżąca u jej podstaw zostanie zatwierdzona;
  • podczas wykonywania kodu okaże się, że wiersze 67–76 nie zostaną wykonane z powodu wyjątku wygenerowanego w wierszu 65. Następnie przejdziemy do wierszy 78–84, aby obsłużyć ten wyjątek;
  • wiersz 78: wyjątek [InterfaceError] występuje, jeśli [sqlalchemy] nie może połączyć się z bazą danych [dbpersonnes]. W wierszu 65 występuje wyjątek [IntegrityError];
  • wiersz 80: wyświetlany jest błąd;
  • w wierszach 82–84: jeśli sesja istnieje, zostaje ona anulowana. Oznacza to anulowanie transakcji bazowej;
  • wiersze 85–88: w każdym przypadku, niezależnie od tego, czy wystąpił błąd, czy nie, sesja jest zamykana w celu zwolnienia zasobów;

Wyniki wykonania są następujące:


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
  • wiersze 2–3: lista osób po pierwszym wstawieniu;
  • wiersz 5: wyjątek [IntegrityError], który wystąpił podczas dodawania dwóch osób o tym samym imieniu i nazwisku;
  • wiersze 6–7: należy zwrócić uwagę na zlecenie SQL, które zakończyło się niepowodzeniem. Jest to zlecenie o numerze INSERT z następującymi parametrami: zlecenie [sqlalchemy] dodało obie osoby za pomocą jednego zlecenia INSERT. Widać tutaj, że próbował zoptymalizować wysłane zlecenia SQL;

Teraz sprawdźmy, za pomocą phpMyAdmin, zawartość tabeli [personnes]:

Image

W [6] widać, że tabela jest pusta. Nie ma tam nawet pierwszej osoby, którą skrypt umieścił w sesji. Wynika to z faktu, że sesja ta odbywała się w ramach transakcji, a transakcja ta została cofnięta w klauzuli [except] skryptu [main].

Przejdźmy teraz do wprowadzenia następującej zmiany w skryptcie [main]:


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

Po dodaniu osoby w wierszu 2 usuwamy komentarz z wiersza 3. Operacja [session.commit] zatwierdzi transakcję bazową i rozpocznie się nowa transakcja. Po wykonaniu zawartość tabeli [personnes] wygląda następująco:

Image

W tabeli [6] widać, że pierwsze wstawienie zostało zachowane. Wynika to z faktu, że zostało ono wykonane w ramach transakcji 1, a błąd, który nastąpił później, wystąpił w ramach transakcji 2.

19.3. Skrypty 02: mapowania dla pliku [sqlalchemy]

Image

Skrypty 02 są odmianą skryptów 01. Staramy się wprowadzić jak najwięcej ustawień w [config.py]. Teraz konfigurujemy w nim środowisko aplikacji [sqlalchemy]:


def configure():
    # ścieżka bezwzględna odnosząca się do ścieżek względnych konfiguracji
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # ścieżki bezwzględne zależności
    absolute_dependencies = [
        # BaseEntity, MyException, Osoba, Narzędzia
        f"{root_dir}/classes/02/entities",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

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

    # połączenie z bazą danych MySQL
    engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

    # metadane
    metadata = MetaData()

    # tabela
    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')
                            )

    # mapowanie
    from Personne import Personne

    mapper(Personne, personnes_table, properties={
        'id': personnes_table.c.id,
        'imię: personnes_table.c.prenom,
        'nazwisko: personnes_table.c.nom,
        'wiek: personnes_table.c.age
    })

    # fabryka sesji
    Session = sessionmaker()
    Session.configure(bind=engine)

    # informacje te umieszczamy w konfiguracji
    config = {}
    config["Session"] = Session
    config["metadata"] = metadata
    config["engine"] = engine
    config["personnes_table"] = personnes_table

    # konfiguracja klas
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # uruchamiamy konfigurację
    return config

Komentarze

  • wiersze 2–12: konfiguracja ścieżki Python;
  • wiersze 14–45: konfiguracja środowiska [sqlalchemy];
  • wiersze 47–52: środowisko [sqlalchemy] jest dodawane do słownika konfiguracyjnego;
  • wiersze 54–56: konfiguracja klasy [Personne];

Po tej konfiguracji skrypt [main] wygląda następująco:


# konfigurujemy aplikację
import config

config = config.configure()

# ścieżka systemowa jest skonfigurowana – wykonujemy importy
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError

from Personne import Personne

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

    # usunięcie tabeli [personnes]
    session.execute("drop table if exists personnes")

    # odtworzenie tabeli na podstawie mapowania
    config["metadata"].create_all(config["engine"])

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

    # sprawdzenie poprawności obu wstawień
    session.commit()

    # jedno zapytanie
    personnes = session.query(Personne).all()

    # wyświetlenie
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # dwa kolejne wstawienia, z których drugie zakończyło się niepowodzeniem
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))

    # jedno zapytanie
    personnes = session.query(Personne).all()

    # wyświetlenie
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # weryfikacja sesji
    session.commit()

except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
    # wyświetlenie
    print(f"L'erreur suivante s'est produite : {erreur}")
    # anulowanie ostatniej sesji
    if session:
        print("rollback...")
        session.rollback()
finally:
    # wyświetlanie
    print("Travail terminé...")
    # zwolnienie zasobów sesji
    if session:
        session.close()

Wyniki wykonania są następujące:


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

W phpMyAdmin tabela [personnes] przybrała następujący kształt:

Image

Teraz przyjrzyjmy się tabeli [personnes] wygenerowanej przez [sqlalchemy]:

Image

  • w tabeli [6] widoczne są typy użyte dla poszczególnych kolumn;
  • w [7] widać, że kolumna [id] posiada atrybut [AUTO_INCREMENT]. Oznacza to, że podczas wstawiania wiersza do tabeli, jeśli wiersz ten nie zawiera wartości dla kolumny [id], zostanie ona wygenerowana przez MySQL w sposób przyrostowy: 1, 2, 3, … Ta właściwość pozwala nam nie martwić się o wartość klucza głównego podczas wstawiania danych do tabeli: pozostawiamy jej generowanie funkcji MySQL;
  • w [8] widać, że kolumna [id] jest kluczem głównym;
  • w polu [9] występuje ograniczenie unikalności dotyczące pól [nom, prenom];

19.4. Skrypty 03: obsługa elementów sesji [sqlalchemy]

Image

Plik konfiguracyjny [config] jest taki sam jak w poprzednim przykładzie. W skrypcie [main] wykonuje się standardowe operacje [INSERT, UPDATE, DELETE, SELECT] na tabeli [personnes] przy użyciu metod z [sqlalchemy]:


# konfiguracja aplikacji
import config

config = config.configure()

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

# wyświetla zawartość tabeli [personnes]
def affiche_table(session: Session):
    print("----------------")
    # zapytanie
    personnes = session.query(Personne).all()
    # wyświetlanie
    affiche_personnes(personnes)

# wyświetla listę osób
def affiche_personnes(personnes: list):
    print("----------------")
    # wyświetlanie
    for personne in personnes:
        print(personne)


# główna ---------------------------
session = None
try:
    # sesja
    session = config["Session"]()

    # usunięcie tabeli [personnes]
    # checkfirst=True: najpierw sprawdza, czy tabela istnieje
    config["personnes_table"].drop(config["engine"], checkfirst=True)

    # ponowne utworzenie tabeli na podstawie mapowania
    config["metadata"].create_all(config["engine"])

    # wstawianie danych
    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}))

    # wyświetlanie zawartości sesji
    affiche_table(session)

    # lista osób w porządku alfabetycznym według nazwisk, a w przypadku identycznych nazwisk – w porządku alfabetycznym według imion
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # wyświetlanie
    affiche_personnes(personnes)

    # lista osób w przedziale wiekowym [20,40] w porządku malejącym według wieku
    # a następnie, przy tym samym wieku, w porządku alfabetycznym według nazwisk, a przy identycznych nazwiskach – w porządku alfabetycznym według imion
    personnes = session.query(Personne). \
        filter(Personne.âge >= 20, Personne.âge <= 40). \
        order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())

    # wyświetlanie
    affiche_personnes(personnes)

    # dodanie pani Bruneau
    bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
    session.add(bruneau)
    # zmiana jej wieku
    bruneau.âge = 47

    # lista osób o nazwisku Bruneau
    personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()

    # wyświetlenie
    affiche_personnes([personne])

    # usunięcie pani Bruneau
    session.delete(personne)

    # lista osób o nazwisku Bruneau
    personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")

    # wyświetlenie
    affiche_personnes(personnes)

    # potwierdzenie sesji
    session.commit()

except (DatabaseError, InterfaceError, IntegrityError) as erreur:
    # wyświetlanie
    print(f"L'erreur suivante s'est produite : {erreur}")
    # anulowanie ostatniej sesji
    if session:
        session.rollback()

finally:
    # wyświetlanie
    print("Travail terminé...")
    # zwolnienie zasobów sesji
    if session:
        session.close()

Komentarze

  • wiersze 20–25: funkcja [affiche_personnes] wyświetla elementy listy osób;
  • wiersze 12–18: funkcja [affiche_table] wyświetla zawartość tabeli [personnes];
  • wiersze 34–36: usuwa się tabelę [personnes]. W przeciwieństwie do poprzednich wersji nie stosuje się polecenia SQL, lecz metodę [sqlalchemy]:
    • config["personnes_table"] to obiekt [Table] opisujący tabelę [personnes];
    • config["engine"] to ciąg połączenia z bazą danych [dbpersonnes];
    • parametr o nazwie [checkfirst=True] określa, że operacja ma zostać wykonana tylko wtedy, gdy istnieje tabela [personnes];
  • wiersze 38–39: tabela [personnes] zostaje utworzona ponownie;
  • wiersze 41–44: do sesji dodano trzy osoby. Należy pamiętać, że niekoniecznie zostaną one natychmiast wstawione do tabeli [personnes]. Zależy to od strategii [sqlalchemy], która ma na celu zapewnienie wydajności;
  • wiersze 46–47: wyświetlana jest zawartość tabeli [personnes]. Jeśli trzy osoby nie zostały jeszcze dodane, to następuje to teraz w wyniku tego zapytania;
  • wiersze 49–50: przykład zastosowania metody [order_by], która pozwala wyświetlić wyniki zapytania w określonej kolejności. Składnia [order_by(critère1, critère2)] wyświetla wyniki najpierw według kryterium [critère1], a gdy wiersze mają tę samą wartość [critère1], są one sortowane według kryterium [critère2]. Można podać kilka kryteriów w następujący sposób:
  • wiersze 55–59: wprowadzają pojęcie filtru za pomocą metody [filter]. Zapis [filter(critère1, critère2)] tworzy logiczne „ET” (AND) pomiędzy zastosowanymi kryteriami;
  • wiersze 64–67: logowana jest nowa osoba;
  • wiersze 70–71: kolejny przykład zapytania z filtrem. Funkcja [func.lower(param)] zwraca [param] pisane małymi literami. Dostępne są również inne funkcje oznaczone jako [func.xx]. W wyrażeniu w wierszu 71:
    • [session.query.filter] zwraca listę obiektów [Personne];
    • [session.query.filter.first] zwraca pierwszy element tej listy;
  • wiersz 77: usuwa się element z sesji;
  • wiersz 86: sesja zostaje zatwierdzona;

Wyniki wykonania są następujące:

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
  • wiersze 4–6: zawartość sesji;
  • wiersze 8–10: zawartość sesji w porządku malejącym według nazwisk;
  • wiersze 12–13: zawartość sesji dla osób, których wiek mieści się w przedziale [20, 40];
  • wiersz 15: osoba o nazwisku „bruneau”;

W phpMyAdmin zawartość tabeli [personnes] po zakończeniu wykonywania wygląda następująco:

Image

19.5. Skrypty 04: korzystanie z bazy danych [PostgreSQL]

Image

Folder [04] jest kopią folderu [03]. Zmienia się tylko jedną rzecz – ciąg połączenia w pliku [config]:


    # połączenie z bazą danych PostgreSQL
    engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")

Teraz ten ciąg połączenia wskazuje bazę danych [dbpersonnes] z pliku SGBD [PostgreSQL]. Należy zwrócić uwagę na użycie łącznika [psycopg2]. Musi on być zainstalowany.

Uruchomienie skryptu [main] daje następujące wyniki:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/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

Po użyciu narzędzia [pgAdmin] (patrz akapit dotyczący |pgAdmin|) tabela [personnes] ma następujący stan:

Image

Tabela [personnes] została wygenerowana przy użyciu następującego kodu SQL:

Image

  • W tabeli [4-5] widać, że kolumna [id] jest kluczem głównym. Widać również, że ma ona wartość domyślną [mot clé DEFAULT], co oznacza, że jeśli wstawimy wiersz bez klucza głównego, zostanie on wygenerowany przez SGBD. Jest to częsty sposób działania: pozwalamy, aby SGBD generował klucze główne;

Ta wersja 05 skryptów [sqlalchemy] dobrze pokazuje, jak łatwo jest przejść z jednego skryptu SGBD na drugi: wystarczyło zmienić ciąg połączenia w skrypcie konfiguracyjnym. Nic więcej się nie zmieniło. Jeśli porównamy typy kolumn [id, nom, prenom, age] powyżej z typami w tabeli MySQL z przykładu |02|, widać, że są one różne. [sqlalchemy] dostosowuje je do używanego SGBD. Ta łatwość dostosowania do nowego pliku SGBD stanowi wystarczający powód, aby zastosować plik [sqlalchemy] lub inny plik ORM.

19.6. Skrypty 05: kompletny przykład

Image

Analizowany przykład jest kontynuacją przykładu omówionego w akapicie |troiscouches-v01|. Przykład ten przedstawiał trójwarstwową architekturę [ui, métier, dao], która operowała na encjach [Classe, Elève, Matière, Note]. Entities były zakodowane na stałe w warstwie [dao]. Teraz umieszczamy je w bazie danych. Wykorzystamy dwie warstwy SGBD: MySQL i PostgreSQL.

19.6.1. Architektura aplikacji

Architektura aplikacji będzie wyglądać następująco:

Image

  • w [1-3] znajdują się warstwy [ui, métier, dao], które występowały już w przykładzie |troiscouches-v01|. Warstwa [dao] komunikuje się teraz z warstwą [ORM];
  • warstwy [1-5] są zaimplementowane za pomocą kodu w języku Python;

19.6.2. Bazy danych

Tworzymy bazę danych o nazwie MySQL, należącą do użytkownika [admecole], który posiada hasło [mdpecole]. W tym celu postępujemy zgodnie z procedurą opisaną w akapicie |création d'une base de données|:

Image

Image

  • w [1], baza [dbecole] bez tabel [3];
  • w bazie [7] użytkownik [admecole] posiada wszystkie uprawnienia do tej bazy danych;

To samo robimy z plikami SGBD i PostgreSQL. Tworzymy bazę o nazwie [dbecole], należącą do użytkownika [admecole], którego hasło brzmi [mdpecole]. W tym celu postępujemy zgodnie z procedurą opisaną w akapicie |création d'une base de données|:

Image

  • w [1], baza [dbecole];
  • w [2] – użytkownik [admecole];
  • w [3-4] baza [dbecole] jest własnością użytkownika [admecole];

19.6.3. Podmioty przetwarzane przez aplikację

W aplikacji |troiscouches v01| obsługiwane były następujące encje (patrz |entités|). To właśnie te encje zostaną zapisane w wyżej wymienionych bazach danych. Nie będziemy duplikować tych encji w nowej aplikacji. Pobierzemy je z miejsc, w których są już zdefiniowane.

Klasa [Classe]:


# importy
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator klasy
        # nazwa: nazwa klasy
        return BaseEntity.get_allowed_keys() + ["nom"]

    # metoda pobierająca
    @property
    def nom(self: object) -> str:
        return self.__nom

    # metody ustawiające
    @nom.setter
    def nom(self: object, nom: str):
        # nazwa musi być niepustym ciągiem znaków
        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")

Klasa [Elève]:


# importy
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator ucznia
        # nazwisko: nazwisko ucznia
        # imię: imię ucznia
        # klasa: klasa ucznia
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # metody pobierające
    @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

    # metody ustawiające
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # nazwisko musi być niepustym ciągiem znaków
        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:
        # imię musi być niepustym ciągiem znaków
        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:
            # oczekuje się typu Klasa
            if isinstance(value, Classe):
                self.__classe = value
            # lub typu „dict”
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # lub typu 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}")

Klasa [Matière]:


# importy
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator przedmiotu
        # nazwa: nazwa przedmiotu
        # współczynnik: współczynnik przedmiotu
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # metoda pobierająca
    @property
    def nom(self: object) -> str:
        return self.__nom

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

    # ustawki
    @nom.setter
    def nom(self: object, nom: str):
        # nazwa musi być niepustym ciągiem znaków
        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):
        # współczynnik musi być liczbą rzeczywistą >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # błąd?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

Klasa [Note]:


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


class Note(BaseEntity):
    # atrybuty wykluczone ze stanu klasy
    excluded_keys = []

    # właściwości klasy
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identyfikator oceny
        # wartość: sama ocena
        # uczeń: uczeń (typu Uczeń), którego dotyczy ocena
        # przedmiot: przedmiot (typu Przedmiot), którego dotyczy ocena
        # Obiekt „Ocena” to zatem ocena ucznia z danego przedmiotu
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # metody pobierające
    @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

    # metody pobierające
    @valeur.setter
    def valeur(self: object, valeur: float):
        # ocena musi być liczbą rzeczywistą z przedziału od 0 do 20
        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:
            # oczekuje się typu „Uczeń”
            if isinstance(value, Elève):
                self.__élève = value
            # lub typu dict
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # lub typu 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:
            # oczekuje się typu „Przedmiot”
            if isinstance(value, Matière):
                self.__matière = value
            # lub typu „dict”
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # lub typu json
            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. Konfiguracja

Image

Konfiguracja została podzielona na kilka plików:

  • ogólna konfiguracja w pliku [config.py]: określa ścieżkę Python Path aplikacji i instancjonuje warstwy architektury;
  • konfiguracja pliku [sqlalchemy] w pliku [config_database]: dokonuje ona mapowania klas na tabele;
  • warstwy aplikacji są skonfigurowane w pliku [config_layers];

Plik [config] ma następującą treść:


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

    # krok 1 ---
    # ustawiamy ścieżkę Python Path aplikacji
    # ścieżka bezwzględna do folderu zawierającego ten skrypt
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ścieżka bezwzględna, do której odnoszą się ścieżki względne w konfiguracji
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # ścieżki bezwzględne zależności
    absolute_dependencies = [
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # projekt trójwarstwowy v01
        f"{root_dir}/troiscouches/v01/interfaces",
        f"{root_dir}/troiscouches/v01/services",
        f"{root_dir}/troiscouches/v01/entities",
        # dokumentacja niniejszego projektu
        script_dir,
        f"{script_dir}/../services",
    ]

    # aktualizacja ścieżki systemowej
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # etap 2 ------
    # konfiguracja bazy danych
    import config_database
    config = config_database.configure(config)

    # etap 3 ------
    # instancjonowanie warstw aplikacji
    import config_layers
    config = config_layers.configure(config)

    # przekazujemy konfigurację
    return config
  • wiersze 4–27: tworzenie ścieżki Python Path aplikacji;
  • wiersze 29–32: konfiguracja pliku [sqlalchemy];
  • wiersze 34–37: konfiguracja warstw aplikacji;

Plik [config_database] ma następującą treść:


def configure(config: dict) -> dict:
    # konfiguracja ['sgbd'] to nazwa używanego SGBD
    # MySQL: MySQL
    # pgres: PostgreSQL

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

    from sqlalchemy.orm import mapper, relationship, sessionmaker

    # ciągi połączeń z wykorzystywanymi bazami danych
    engines = {
        'mysql': „mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole”,
        'pgres': „postgresql+psycopg2://admecole:mdpecole@localhost/dbecole”
    }
    # ciąg połączenia z używaną bazą danych
    engine = create_engine(engines[config['sgbd']])

    # metadane
    metadata = MetaData()

    # tabele bazy danych
    tables = {}
    # mapowane klasy
    from Classe import Classe
    from Elève import Elève
    from Note import Note
    from Matière import Matière

    # tabela klas
    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,
        'nazwa: classes_table.c.nom
    })

    # tabela uczniów
    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),
              # uczeń należy do klasy
              Column('classe_id', Integer, ForeignKey('classes.id')),
              )
    # mapowanie
    mapper(Elève, tables['élèves'], properties={
        'id': élèves_table.c.id,
        'nazwisko: élèves_table.c.nom,
        'imię: élèves_table.c.prénom,
        'klasa: relationship(Klasa, backref="uczniowie", lazy="select")
    })

    # spis treści
    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)
              )
    # mapowanie
    mapper(Matière, tables['matières'], properties={
        'id': matières_table.c.id,
        'nazwa: matières_table.c.nom,
        "coefficient": matières_table.c.coefficient
    })

    # tabela ocen
    tables['notes'] = notes_table = \
        Table("notes", metadata,
              Column('id', Integer, primary_key=True),
              Column('valeur', Float, nullable=False),
              # ocena dotyczy ucznia
              Column('élève_id', Integer, ForeignKey('élèves.id')),
              # ocena dotyczy przedmiotu
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # mapowanie
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'wartość: notes_table.c.valeur,
        'uczeń': relacja(Uczeń, backref="notatki", lazy="select"),
        'przedmiot': relacja(Przedmiot, backref="oceny", lazy="select")
    })

    # konfiguracja encji [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']

    # fabryka sesji
    Session = sessionmaker()
    Session.configure(bind=engine)

    # sesja
    session = Session()

    # zapisuje się pewne informacje w słowniku konfiguracyjnym
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # udostępnianie konfiguracji
    return config

Komentarze

  • wiersze 1–4: funkcja [configure] otrzymuje jako parametr słownik. Wykorzystywany jest wyłącznie klucz [sgbd]. Ma ona wartość [mysql], jeśli bazą jest baza MySQL, oraz [pgres], jeśli bazą jest baza PostgreSQL;
  • wiersze 6–9: import elementów z bazy [sqlalchemy]. Skrypt [config_database] tworzy mapowania między tabelami bazy [dbecole] a encjami [Classes, Elève, Matière, Note]. W tabeli dane encji są zawarte w jednym wierszu. W kodzie w języku Python są one zawarte w obiekcie. Stąd nazwa ORM (Object Relational Mapper): skrypt ORM tworzy mapowanie (powiązanie) między wierszami relacyjnej bazy danych a obiektami. W tej aplikacji mamy cztery encje [Classe, Elève, Matière, Note], które zostaną powiązane z czterema tabelami [classes, élèves, matières, notes]. Należy zauważyć, że nazwy tabel mogą zawierać znaki akcentowane;
  • wiersze 11–17: ciąg połączenia z wykorzystywaną bazą danych. Zależy on od elementu config[‘sgbd’];
  • wiersze 24–28: elementy aplikacji, które będą przedmiotem mapowania [sqlalchemy]. Gdy te wiersze zostaną wykonane, ścieżka Python Path zostanie już ustalona przez skrypt [config];
  • wiersze 30–40: mapowanie między encją [Classe] a tabelą [classes];
  • wiersze 30–35: tabela [classes] jest zdefiniowana z klasą [Table] z [sqlalchemy]. Wskazujemy, że tabela ta ma dwie kolumny:
    • kolumna [id], która jest kluczem głównym i stanowi numer klasy, wiersz 33;
    • kolumna [nom], zawierająca nazwę klasy, wiersz 34;
  • wiersze 31–32: należy zauważyć, że składnia x=y=z jest poprawna w języku Python: wartość z jest przypisywana do y, a następnie wartość y do x;
  • wiersze 37–40: wymieniono powiązania między kolumnami tabeli [classes] a właściwościami encji [Classe];
  • wiersze 42–57: mapowanie między encją [Elève] a tabelą [élèves];
  • wiersze 51–57: tabela [élèves] jest zdefiniowana przy użyciu klasy [Table] z [sqlalchemy]. Wskazujemy, że tabela ta ma cztery kolumny:
    • kolumna [id], która jest kluczem głównym i zawiera numer ucznia, wiersz 45;
    • kolumna [nom], zawierająca nazwisko ucznia, wiersz 46;
    • kolumna [prénom], zawierająca imię ucznia, wiersz 47. Należy pamiętać, że nazwa kolumny może zawierać znaki z akcentami;
    • wiersz 49, kolumna [classe_id], która będzie zawierała numer klasy, do której należy uczeń. Nazywa się to kluczem obcym. [élèves.classe_id] jest kluczem obcym (ForeignKey) w kolumnie [classes.id]. Oznacza to, że wartość [élèves.classe_id] musi istnieć w kolumnie [classes.id];
  • wiersze 51–57: wymieniono powiązania między kolumnami tabeli [élèves] a właściwościami encji [Elève]:
    • wiersze 53–55 są łatwe do zrozumienia;
    • wiersz 56 jest trudniejszy: określa on, że wartość właściwości [Elève.classe] jest obliczana na podstawie relacji klucza obcego łączącej tabele [élèves] i [classes]. Parametry funkcji [relationship] są następujące:
      • [Classe]: jest to nazwa encji, z którą encja [Elève] pozostaje w relacji klucza obcego. Relacja ta musi być odzwierciedlona w tabeli [élèves] poprzez obecność klucza obcego w tabeli [classes]. Wiemy, że taki klucz istnieje;
      • [backref="élèves"]: nazwa właściwości, która zostanie dodana do encji [Classe]. [Classe.élèves] będzie listą wszystkich uczniów w klasie. Ta właściwość nie może już istnieć. Jeśli już istnieje, wystarczy po prostu wybrać tutaj inną nazwę dla [backref]. Programista nie musi zajmować się tą właściwością. Zajmie się tym [sqlalchemy]. Musi jedynie wiedzieć, że istnieje ona, dodana przez [sqlalchemy], i że może z niej korzystać w swoim kodzie;
      • [lazy=’select’]: oznacza to, że ORM nie powinien próbować natychmiast nadawać wartości właściwości [Elève.classe]. Powinien szukać jej wartości tylko wtedy, gdy kod wyraźnie o to poprosi. A zatem:
      • jeśli kod żąda listy wszystkich uczniów, zostaną oni zwróceni, ale ich właściwość [classe] nie zostanie obliczona;
      • nieco później kod interesuje się konkretnym uczniem [e] i odwołuje się do jego klasy [e.classe]. Odwołanie to spowoduje, że [sqlalchemy] wykona zapytanie do bazy danych w celu pobrania klasy ucznia, co odbywa się w sposób przezroczysty dla programisty;
      • ponadto wprowadzenie [lazy=’select’] ma na celu uniknięcie zbędnych zapytań do bazy danych;
  • wiersz 56: gdy ORM pobiera wiersz z tabeli [élèves], pobiera informacje [id, nom, prénom, classe_id]. Na tej podstawie musi utworzyć obiekt Uczeń(id, nazwisko, imię, klasa). W przypadku właściwości [id, nom, prénom] nie stanowi to żadnej trudności. W przypadku właściwości [classe] sprawa jest bardziej skomplikowana. Jej wartością jest odwołanie do obiektu typu [Classe]. Jednak obiekt ORM zawiera jedynie informację o obiekcie [élèves.classe_id]. Ponieważ [élèves.classe_id] jest kluczem obcym w kolumnie [classes.id], nakazuje się mu tutaj wykorzystanie tej relacji w celu pobrania z tabeli [classes] wiersza o identyfikatorze id=[élèves.classe_id] (który z pewnością istnieje) oraz utworzyć na podstawie tego wiersza obiekt [Classe] oczekiwany przez właściwość [Elève.classe];
  • wiersze 59–71: mapowanie między encją [Matière] a tabelą [matières];
  • wiersze 59–65: definicja tabeli [sqlalchemy] o nazwie [matières];
  • wiersze 66–71: wymieniono powiązania między kolumnami tabeli [matières] a właściwościami encji [Matière]. Nie ma tu żadnych trudności;
  • wiersze 73–90: mapowanie między encją [Note] a tabelą [notes];
  • wiersze 73–82: definicja tabeli [sqlalchemy] o nazwie [notes]. Posiada ona dwa klucze obce:
    • w wierszu 79 kolumna [notes.élève_id] pobiera wartości z kolumny [élèves.id]]. Ten klucz obcy odzwierciedla fakt, że ocena należy do konkretnego ucznia;
    • wiersz 81, kolumna [notes.matière_id] pobiera swoje wartości z kolumny [matières.id]. Ten klucz obcy odzwierciedla fakt, że ocena dotyczy konkretnego przedmiotu;
  • wiersze 84–90: powiązanie między encją [Note] a tabelą [notes]:
    • wiersz 88: właściwość [Note.élève] musi przyjmować jako wartość instancję typu [Elève]. W wierszu tabeli [notes] dla obiektu ORM znajduje się jedynie kolumna [notes.élève_id], która odwołuje się do kolumny [élèves.id]. W tym przypadku zaleca się wykorzystanie tej relacji klucza obcego w celu odnalezienia instancji [Elève], dla której znana jest ocena. Ponadto [relationship(Elève, backref="notes", …)] utworzy nową właściwość [Elève.notes], która będzie listą ocen ucznia. Ta właściwość nie może już istnieć w klasie [Elève];
    • wiersz 89: właściwość [Note.matière] musi mieć jako wartość instancję typu [Matière]. Wiersz tabeli ORM zawiera w tabeli [notes] jedynie kolumnę [notes.matière_id], która odwołuje się do kolumny [matières.id]. W tym przypadku zaleca się wykorzystanie tej relacji klucza obcego w celu odnalezienia instancji [Matière], dla której znana jest ocena. Ponadto [relationship(Matière, backref="notes", …)] utworzy nową właściwość [Matière.notes], która będzie listą ocen z danego przedmiotu. Ta właściwość nie może już istnieć w klasie [Matière];
  • wiersze 92–96: dla każdej jednostki pochodnej od [BaseEntity] definiuje się listę właściwości, które należy wykluczyć ze słownika właściwości tej jednostki (BaseEntity.asdict). Zauważyliśmy, że [sqlalchemy] dodawało właściwość [_sa_instance_state] do wszystkich mapowanych encji. Nie chcemy, aby ta właściwość znajdowała się w słowniku właściwości. Ponadto zauważyliśmy, że poprzednie mapowania dodały nowe właściwości do encji:
    • [Elève.notes]: wszystkie oceny ucznia;
    • [Classe.élèves]: wszyscy uczniowie w klasie;
    • [Matière.notes]: wszystkie oceny z przedmiotu;

Zazwyczaj nie chcemy, aby te właściwości były dodawane do stanu encji. Obliczenie ich wartości wiąże się bowiem z pewnym kosztem SQL, a wartość ta jest często zbędna. Jeśli więc pobieramy ucznia o imieniu „X”:

  • (ciąg dalszy)
    • ORM zwróci encję [Elève(id, nom, prénom, classe, notes)]. Z powodu [lazy=’select’] właściwości [classe, notes] powiązane z kluczami obcymi w bazie nie zostaną obliczone;
    • teraz, jeśli wyświetlę ciąg jSON tego ucznia, wiadomo, że będzie to ciąg jSON ze słownika [asdict] tej encji. Jeśli w nim znajdują się właściwości [classe] i [notes], to [sqlalchemy] będzie zmuszony wysłać zapytania do SQL w celu obliczenia ich wartości. Jest to kosztowne. Jeśli można uniknąć tych zapytań, to lepiej;
    • w tym przypadku wykluczyliśmy wszystkie właściwości powiązane z kluczem obcym;
  • wiersze 98–100: instancjonowanie i konfiguracja obiektu [Session factory] (factory = fabryka). Obiekt [Session] służy do tworzenia sesji [sqlalchemy] powiązanych z transakcjami;
  • wiersze 102–103: utworzenie sesji sqlalchemy];
  • wiersz 106: niektóre elementy konfiguracji [sqlalchemy] są umieszczane w globalnym słowniku konfiguracji aplikacji;
  • wiersz 109: zwracamy ten słownik;

Plik [config_layers] konfiguruje warstwy aplikacji:


def configure(config: dict) -> dict:
    # instancjonowanie warstwy [dao]
    from DatabaseDao import DatabaseDao
    dao = DatabaseDao(config)

    # instancjonowanie warstwy [métier]
    from Métier import Métier
    métier = Métier(dao)

    # instancjonowanie warstwy [ui]
    from Console import Console
    ui = Console(métier)

    # dodajemy warstwy do konfiguracji
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # przywracamy konfigurację
    return config
  • wiersz 1: funkcja [configure] otrzymuje słownik globalnej konfiguracji aplikacji;
  • wiersze 2–12: tworzone są instancje warstw aplikacji;
  • wiersze 15–17: odniesienia do warstw są umieszczane w globalnej konfiguracji;
  • wiersz 20: zwracana jest nowa konfiguracja;

19.6.5. Warstwa [dao] – 1

Image

Należy tutaj zrozumieć, że warstwa [dao] [3] komunikuje się zORM, [sqlalchemy] i [4], skonfigurowanymi zgodnie z opisem w poprzednim akapicie. Spośród trzech warstw [ui, métier, dao] aplikacji |troiscouches v01| tylko warstwa [dao] wymaga przepisania. Warstwy [ui, métier] pozostają bez zmian.

Implementacja warstwy [dao] została umieszczona w folderze [services]:

Image

[InterfaceDatabaseDao] stanowi interfejs warstwy [dao]:


from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao


class InterfaceDatabaseDao(InterfaceDao, ABC):

    # inicjalizacja bazy danych
    @abstractmethod
    def init_database(self, data: dict):
        pass
  • wiersz 6: interfejs [InterfaceDatabaseDao] dziedziczy zarówno po klasie [ABC], będąc klasą abstrakcyjną, jak i po interfejsie [InterfaceDao] z projektu |troiscouches v01|;
  • wiersze 8–11: do metod odziedziczonych po klasie [InterfaceDao] dodano metodę [init_database]. Jej zadaniem będzie zainicjowanie bazy danych danymi ze słownika [data], które są jej przekazywane jako parametr w wierszu 10;

Przypomnijmy, że interfejs [InterfaceDao] wyglądał następująco:


# importy
from abc import ABC, abstractmethod

# interfejs DAO
from Elève import Elève


class InterfaceDao(ABC):
    # lista klas
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # lista uczniów
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # lista przedmiotów
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

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

    # lista ocen ucznia
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # wyszukiwanie ucznia według identyfikatora
    @abstractmethod
    def get_élève_by_id(self: object, élève_id: int) -> Elève:
        pass

Implementacja warstwy [dao] wygląda następująco:


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):
        # baza danych = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

  • wiersz 11: klasa [DatabaseDao] implementuje interfejs [InterfaceDatabaseDao];
  • wiersze 13–16: konstruktor klasy. Otrzymuje on jako parametr słownik konfiguracji aplikacji;
  • wiersz 15: zapisujemy konfigurację [sqlalchemy];
  • wiersz 16: zapisujemy sesję [sqlalchemy], za pomocą której będziemy obsługiwać bazę danych;
  • wiersz 18: metoda [init_database] inicjuje bazę danych przy użyciu słownika [data];

Słownik [data] jest zaimplementowany przez następujący skrypt [data.py]:


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

    # instancjonujemy klasy
    classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
    classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
    classes = [classe1, classe2]
    # przedmioty
    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]
    # uczniowie
    é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]
    # oceny uczniów z poszczególnych przedmiotów
    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]
    # grupuje się całość
    data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
    # przekazujemy dane
    return data
  • wiersz 34: słownik, który zostanie przekazany do metody [init_database]. Słownik ten składa się z następujących kluczy (wiersz 32):
    • [élèves]: lista uczniów;
    • [classes]: lista klas;
    • [matières]: lista przedmiotów;
    • [notes]: lista ocen wszystkich uczniów ze wszystkich przedmiotów;

Wróćmy do metody [init_database]:


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

        try:
            # usuwanie istniejących tabel
            # checkfirst=True: najpierw sprawdza, czy tabela istnieje
            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)

            # ponowne utworzenie tabel na podstawie mapowania
            metadata.create_all(engine)

            # wypełnianie tabel
            session = self.session

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

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

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

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

            # zatwierdzenie
            session.commit()
        except (DatabaseError, InterfaceError, IntegrityError) as erreur:
            # anulowanie sesji
            if session:
                session.rollback()
            # przekazywanie wyjątku
            raise MyException(23, f"{erreur}")
  • wiersze 3–6: pobieramy informacje z konfiguracji bazy danych;
  • wiersze 9–14: widzieliśmy, że konfiguracja [sqlalchemy] przypisała cztery encje do czterech tabel [élèves, matières, classes, notes]. Najpierw usuwamy te tabele, jeśli istnieją;
  • wiersze 16–17: odtwarzamy cztery tabele, które właśnie usunęliśmy;
  • wiersze 22–25: umieszczamy wszystkie klasy w sesji;
  • wiersze 27–30: dodajemy wszystkie przedmioty do sesji;
  • wiersze 32–35: dodajemy wszystkich uczniów do sesji;
  • wiersze 37–40: dodajemy wszystkie oceny do sesji;
  • aby dokonać tych dodania, postępowaliśmy zgodnie z określoną kolejnością. Zaczęliśmy od encji, które nie mają powiązań z innymi encjami, a zakończyliśmy na tych, które je mają. Dzięki temu, gdy dodajemy uczniów do sesji, klasy, do których oni należą, są już w sesji;
  • wiersz 43: sesja [sqlalchemy] została zatwierdzona. Po tej operacji mamy pewność, że wszystkie dane w sesji zostały zsynchronizowane z bazą danych. Innymi słowy, trafiły one do tabel. Było to możliwe dzięki mapowaniom, które zostały utworzone w konfiguracji [sqlalchemy]. [sqlalchemy] wie, w jaki sposób każda jednostka powinna być zapisana w tabelach. [sqlalchemy] wygenerowało również klucze obce, które mogą występować w tabelach;
  • wiersze 44–49: w przypadku wystąpienia problemu sesja [sqlalchemy] zostaje przerwana, a w wierszu 49 zgłaszany jest wyjątek;

19.6.6. Inicjalizacja bazy danych

Image

Skrypt [main_init_database] inicjuje bazę danych zawartością skryptu [data.py]. Jego kod wygląda następująco:


# oczekuje się parametru mysql lub pgres
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()

# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})

# ścieżka systemowa jest skonfigurowana – można przeprowadzić importy
from MyException import MyException

# pobierane są dane do wstawienia do bazy
import data
data = data.configure()

# pobierana jest warstwa [dao]
dao = config["dao"]

# ----------- główna
try:
    # utworzenie i zainicjowanie tabel bazy danych
    dao.init_database(data)
except MyException as ex:
    # wyświetlanie błędu
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # zwolnienie zasobów wykorzystanych przez aplikację
    import shutdown
    shutdown.execute(config)
# koniec
print("Travail terminé...")
  • wiersze 1–11: skrypt oczekuje parametru [mysql] lub [pgres], w zależności od tego, czy ma zostać zainicjowana baza MySQL, czy PostgreSQL;
  • wiersze 13–15: aplikacja jest skonfigurowana pod kątem parametru SGBD przekazanego jako argument;
  • wiersze 20–22: pobierane są dane, które mają zostać zapisane w bazie;
  • wiersz 25: warstwa [dao] została już zainicjowana i jest dostępna w konfiguracji aplikacji;
  • wiersz 30: baza danych zostaje zainicjowana;
  • wiersze 34–37: niezależnie od tego, czy wystąpił błąd, czy nie, zwalnia się zasoby aplikacji za pomocą modułu [shutdown];

Moduł [shutdown.py] wygląda następująco:


def execute(config: dict):
    # zwolnienie zasobów wykorzystanych przez aplikację
    sqlalchemy_session = config['database']['session']
    if sqlalchemy_session:
        sqlalchemy_session.close()

Funkcja [shutdown.execute] zamyka sesję [sqlalchemy] wykorzystaną do zainicjowania bazy danych.

Tworzymy pierwszą konfigurację wykonania (patrz |configuration d’exécution|) w celu uruchomienia [main_init_database] wraz z SGBD i MySQL:

Image

Wyniki wykonania tej konfiguracji w phpMyAdmin są następujące:

Image

Image

Image

W przypadku plików SGBD i [PostgreSQL] stosujemy następującą konfigurację wykonania:

Image

Po uruchomieniu wyniki w pliku [pgAdmin] są następujące:

Image

Image

Image

Warto zwrócić uwagę na łatwość, z jaką udało się zmienić plik SGBD.

19.6.7. Warstwa [dao] – 2

Wracamy do klasy [DatabaseDao], która implementuje warstwę [dao]. Jak dotąd pokazaliśmy jedynie implementację metody [init_database]. Teraz przedstawiamy implementację pozostałych metod:


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):
        # baza danych = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

    # lista wszystkich klas
    def get_classes(self: object) -> list:
        # zapytanie
        return self.session.query(Classe).all()

    # lista wszystkich uczniów
    def get_élèves(self: object) -> list:
        # zapytanie
        return self.session.query(Elève).all()

    # lista wszystkich przedmiotów
    def get_matières(self: object) -> list:
        # zapytanie
        return self.session.query(Matière).all()

    # lista ocen wszystkich uczniów
    def get_notes(self: object) -> list:
        # zapytanie
        return self.session.query(Note).all()

    # lista ocen konkretnego ucznia
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # wyszukiwanie ucznia – jeśli nie istnieje, zgłaszany jest wyjątek
        # pozwala się na powrót do strony głównej
        élève = self.get_élève_by_id(élève_id)
        # pobieramy jego oceny (lazy loading)
        notes = élève.notes
        # zwracamy słownik
        return {"élève": élève, "notes": notes}

    # uczeń zidentyfikowany na podstawie numeru
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # szukamy ucznia
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # czy udało się znaleźć?
        if élèves:
            return élèves[0]
        else:
            raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")

    # uczeń zidentyfikowany na podstawie imienia
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # szukamy ucznia
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # czy udało się go znaleźć?
        if élèves:
            return élèves[0]
        else:
            raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")

    # klasa zidentyfikowana na podstawie numeru
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # szukamy klasy
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # czy już znaleźliśmy?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # klasa oznaczona nazwą
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # szukamy klasy
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # czy udało się ją znaleźć?
        if classes:
            return classes[0]
        else:
            raise MyException(14, f"La classe de nom {classe_name} n'existe pas")

    # przedmiot zidentyfikowany na podstawie numeru
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # szukamy przedmiotu
        matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
        # czy udało się znaleźć?
        if matières:
            return matières[0]
        else:
            raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")

    # materiał oznaczony nazwą
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # szukamy substancji
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # czy udało się znaleźć?
        if matières:
            return matières[0]
        else:
            raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
  • wiersze 21–24: metoda [get_classes] ma zwrócić listę klas w szkole. W wierszu 20 używamy zapytania, z którym już się spotkaliśmy;
  • wiersze 26–39: trzy inne podobne metody służące do uzyskania list uczniów, przedmiotów i ocen;
  • wiersze 51–59: metoda [get_élève_by_id] powinna zwrócić ucznia zidentyfikowanego na podstawie jego numeru. Jeśli taki uczeń nie istnieje, metoda ta zgłasza wyjątek;
    • wiersz 54: używamy zapytania z filtrem. Otrzymujemy listę pustą lub zawierającą jeden element;
    • wiersz 57: jeśli pobrana lista nie jest pusta, zwracany jest pierwszy element listy;
    • w przeciwnym razie, w wierszu 59, zgłaszany jest wyjątek;
  • wiersze 41–49: metoda [get_notes_for_élève_by_id] musi zwrócić oceny ucznia zidentyfikowanego na podstawie jego numeru:
    • w wierszu 45 wykorzystuje się metodę [get_élève_by_id] w celu uzyskania encji Ucznia dla danego ucznia;
    • w wierszu 47 wykorzystuje się właściwość [Elève.notes] utworzoną przez mapowanie między encją [Note] a tabelą [notes] (por. akapit |configuration sqlalchemy|), która reprezentuje oceny ucznia;
    • wiersz 49: zwracany jest słownik;
  • wiersze 61–109: seria analogicznych metod umożliwiających:
    • znalezienie ucznia na podstawie jego nazwiska, wiersze 61–69;
    • znalezienie klasy, wiersze 71–89;
    • znalezienie przedmiotu, wiersze 91–109;

19.6.8. Skrypt [main_joined_queries]

Image

Skrypt [main_joined_queries] nosi taką nazwę, ponieważ ma na celu uwidocznienie zapytań wysyłanych niejawnie przez [sqlalchemy] w celu pobrania informacji z wielu tabel. Te ukryte przed programistą zapytania są wysyłane za każdym razem, gdy właściwość encji została powiązana z funkcją [relationship] w mapowaniu tej encji. Na przykład:


    # mapowanie
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'wartość: notes_table.c.valeur,
        'uczeń': relacja(Uczeń, backref="notatki", lazy="select"),
        'przedmiot': relacja(Przedmiot, backref="oceny", lazy="select")
    })

Powyżej przedstawiono mapowanie między encją [Note] a tabelą [notes]:

  • w wierszu 5, gdy właściwość [élève] encji [Note] zostanie zażądana po raz pierwszy, zostanie ona wyszukana w tabeli [élèves] za pomocą zapytania SQL. Dopóki ta właściwość nie zostanie zażądana, pozostaje nieokreślona (lazy load). Po jej uzyskaniu jej wartość pozostaje w pamięci tabeli ORM. Gdy zostanie ona wywołana po raz drugi, obiekt ORM natychmiast zwróci jej wartość bez konieczności wysyłania nowego zapytania SQL. Cały ten proces jest przezroczysty dla programisty;
  • to samo dotyczy właściwości odwrotnej [Elève.notes] (backref), wiersz 5;
  • to samo dotyczy właściwości [Note.matière] i jej właściwości odwrotnej [Matière.notes] (backref), wiersz 6;

Skrypt [main_joined_queries] wygląda następująco:


# oczekuje się parametru mysql lub pgres
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()

# konfigurujemy aplikację
import config
config = config.configure({"sgbd": sgbd})

# ścieżka systemowa jest skonfigurowana – można przeprowadzić importy
from MyException import MyException

# warstwa [dao]
dao = config["dao"]
try:
    # uczeń według identyfikatora
    print("élève id=11 -----------")
    élève = dao.get_élève_by_id(11)
    print(f"élève={élève}")
    # klasa ucznia (lazy loading)
    classe = élève.classe
    print(f"classe de l'élève : {classe}")
    # uczniowie z tej samej klasy (lazy loading)
    print("élèves dans la même classe :")
    for élève in classe.élèves:
        print(f"élève={élève}")

    # uczeń według imienia
    print("élève nom='nom2' -----------")
    print(f"élève={dao.get_élève_by_name('nom2')}")
    # jego klasa (lazy loading)
    print(f"classe de l'élève : {élève.classe}")

    # oceny ucznia
    print("notes de l'élève id=11 -----------")
    # najpierw uczeń
    élève = dao.get_élève_by_id(11)
    # następnie jego oceny (lazy loading)
    for note in élève.notes:
        # ocena
        print(f"note={note}, "
              # przedmiot, z którego uzyskano ocenę (lazy loading)
              f"matière={note.matière}")

    # uczniowie z klasy
    print("élèves de la classe nom='classe1' -----------")
    # najpierw klasa
    classe = dao.get_classe_by_name('classe1')
    # następnie uczniowie (lazy loading)
    for élève in classe.élèves:
        print(élève)

    # to samo dotyczy [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)

    # oceny z przedmiotu
    print("matière de nom='matière1' -----------")
    # najpierw przedmiot
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # następnie oceny z tego przedmiotu (lazy loading)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # to samo dotyczy przedmiotu 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:
    # wyświetla się błąd
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # wyświetla się błąd
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # zwolniono zasoby
    import shutdown
    shutdown.execute(config)

Komentarze wystarczają do zrozumienia kodu.

Tworzymy konfigurację uruchomienia dla MySQL:

Image

Wyniki wykonania są następujące:

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

Aby zrozumieć te wyniki, należy pamiętać, że z słownika encji wykluczono niektóre właściwości (por. |configuration|):


    # konfiguracja encji [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']

Tak więc, gdy wpisujemy [print(f"élève={élève}")] w wierszu 26 kodu, wiersz 1 powyżej informuje nas, że właściwości ['_sa_instance_state', 'notes', 'classe'] nie zostaną wyświetlone. Widać to w wierszu 3 wyników. Wszystkie pozostałe właściwości są wyświetlane. Tak więc, nadal w wierszu 3, odkrywamy nową właściwość [classe_id], która początkowo nie istniała w encji [Elève]. Ta właściwość odpowiada bezpośrednio kolumnie [classe_id] w tabeli [élèves]. W ten sposób [sqlalchemy] dodało następujące właściwości do encji [Elève]: [classe_id, _sa_instance_state, notes]. Należy mieć tego świadomość, zwłaszcza że właściwości te nie mogą już istnieć w mapowanej encji.

Właściwości wykluczone ze słownika encji mają duże znaczenie. Jeśli na przykład nie wykluczy się właściwości [notes, élève] z encji [Elève], to operacja [print(f"élève={élève}")] wyświetli je i w związku z tym, jak właśnie wyjaśniono, spowoduje domyślne zapytania SQL (lazy loading) w celu pobrania wartości tych właściwości. Jeśli, tak jak w tym przypadku, wyświetlana jest lista uczniów, niejawne operacje SQL są wykonywane dla każdego ucznia. Z jednej strony może to być zbędne, a z drugiej strony z pewnością kosztowne pod względem czasu wykonania.

Aby uruchomić skrypt z bazą PostgreSQL, tworzymy następującą konfigurację wykonania:

Image

Wykonanie daje takie same wyniki jak w przypadku MySQL.

19.6.9. Skrypt [main_stats_for_élève]

Image

Skrypt [main_stats_for_élève] to ten sam, który był już używany w aplikacji |troiscouches v01]. Wówczas nosił nazwę [main]. Jest to aplikacja konsolowa umożliwiająca uzyskanie pewnych wskaźników dotyczących ocen ucznia: [moyenne pondérée, min, max, liste]. Wpisuje się ona w następującą architekturę:

Image

W tej architekturze warstwowej jedynie warstwa [dao] uległa zmianie między aplikacją |troiscouches v01| a niniejszą. Ponieważ nowa warstwa [dao] jest zgodna z interfejsem [InterfaceDao] starej warstwy [dao], warstwy [ui, métier] nie wymagają zmian. Można zatem nadal korzystać z warstw zdefiniowanych w aplikacji |troiscouches v01|.

Skrypt [main_stats_for_élève] implementuje warstwę [main] z powyższego schematu w następujący sposób:


# oczekuje się na parametr mysql lub pgres
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()

# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})

# ścieżka systemowa jest skonfigurowana – można przeprowadzić importy
from MyException import MyException

# warstwa [ui]
ui = config["ui"]
try:
    # uruchomienie warstwy [ui]
    ui.run()
except MyException as ex1:
    # wyświetlany jest błąd
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # wyświetlany jest błąd
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # zwolniono zasoby
    import shutdown
    shutdown.execute(config)
  • wiersz 20: pobieramy odwołanie do warstwy [ui] z konfiguracji aplikacji;
  • wiersz 24: uruchamia się okno dialogowe z użytkownikiem za pomocą jedynej metody warstwy [ui];

Konfiguracja uruchomienia dla warstwy PostgreSQL wyglądałaby następująco:

Image

Oto przykład uruchomienia z tą konfiguracją:


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