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:

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:

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

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

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

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:

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]

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:

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

- 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]

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

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

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:
Po użyciu narzędzia [pgAdmin] (patrz akapit dotyczący |pgAdmin|) tabela [personnes] ma następujący stan:

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

- 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

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:

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


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

- 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

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

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

[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

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:

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



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

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



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]

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:

Wyniki wykonania są następujące:
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:

Wykonanie daje takie same wyniki jak w przypadku MySQL.
19.6.9. Skrypt [main_stats_for_élève]

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

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:

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