19. Gebruik van ORM SQLALCHEMY
In het vorige hoofdstuk is aangetoond dat het in bepaalde gevallen mogelijk is om code te schrijven die onafhankelijk is van het gebruikte SGBD, met de volgende architectuur:

In dit hoofdstuk gaan we de ORM (Object Relational Mapper) [sqlalchemy] gebruiken om op een uniforme manier toegang te krijgen tot de SGBD, ongeacht de gebruikte SGBD. Een ORM maakt twee dingen mogelijk:
- het stelt een script in staat om met de SGBD te communiceren zonder SQL-opdrachten te geven;
- het verbergt de specifieke kenmerken van elke SGBD voor het script;
De architectuur ziet er nu als volgt uit:
Het script is nu gescheiden van de connectoren door de ORM. Het communiceert met de ORM via klassen en methoden. Het voert geen SQL-code uit. Dat doet de ORM met de connectoren waarmee hij is verbonden. Hij verbergt de specifieke kenmerken van deze connectoren voor het script. Daardoor is de scriptcode ongevoelig voor een verandering van connector (dus van de SGBD);
De structuur van de onderzochte scripts ziet er als volgt uit:

19.1. Installatie van de ORM [sqlalchemy]
De ORM [sqlalchemy] wordt geleverd in de vorm van een Python-pakket dat in een Python-terminal moet worden geïnstalleerd:
(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. Scripts 01: de basis

- in [1], de scripts die zullen worden bestudeerd. Deze scripts maken gebruik van de klassen uit [2]: BaseEntity, MyException, Personne, Utils;
19.2.1. Configuratie
Het bestand [config] configureert de applicatie als volgt:
def configure():
# root_dir
# absoluut pad als referentie voor de relatieve paden van de configuratie
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute paden van de afhankelijkheden
absolute_dependencies = [
# BaseEntity, MyException, Persoon, Hulpprogramma's
f"{root_dir}/classes/02/entities",
]
# het syspath wordt ingesteld
from myutils import set_syspath
set_syspath(absolute_dependencies)
# configuratie van de klassen
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# de configuratie wordt doorgevoerd
return {}
Opmerkingen
- regel 8: de map met de klassen [BaseEntity, MyException, Personne, Utils] wordt toegevoegd aan het Python-pad;
- regels 12-13: het Python-pad van de applicatie wordt vastgelegd;
- regels 16-17: misschien herinner je je nog dat de klasse |BaseEntity| een klasse-attribuut heeft met de naam [excluded_keys]. Dit attribuut is een lijst waarin we de eigenschappen van de klasse plaatsen die we niet willen zien verschijnen in het woordenboek van die klasse (functie asdict). Hier sluiten we de eigenschap [_sa_instance_state] uit van de toestand van de klasse [Personne]. We zullen straks zien waarom;
19.2.2. Script [démo]
Het script [démo] toont een eerste toepassing van de ORM [sqlalchemy]:
# de configuratie van de applicatie wordt opgehaald
import config
config = config.configure()
# imports
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper
from Personne import Personne
# metadata
metadata = MetaData()
# de tabel
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')
)
# de klasse Personne vóór de mapping
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")
# de mapping
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'voornaam': personnes_table.c.prenom,
'achternaam': personnes_table.c.nom,
'leeftijd': personnes_table.c.age
})
# persoon1 is niet gewijzigd
print(f"personne1={personne1.__dict__}")
# de klasse Person is wel gewijzigd – deze is „uitgebreid“
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")
Opmerkingen
- regels 1-4: de toepassing wordt geconfigureerd;
- regels 6-10: de modules die nodig zijn voor het script worden geïmporteerd;
- regel 13: [MetaData] is een klasse van [sqlalchemy];
- regels 15-22: [Table] is een klasse van [sqlalchemy]. Hiermee kan een tabel uit een database worden beschreven. Hier gaan we de tabel [personnes] beschrijven uit de database MySQL [dbpersonnes] die in hoofdstuk |MySQL| is behandeld;
- regel 16: de eerste parameter [personnes] is de naam van de beschreven tabel;
- regel 16: de tweede parameter [metadata] is de instantie [MetaData] die op regel 13 is aangemaakt;
- regels 17-22: elk van de volgende parameters beschrijft een kolom van de tabel met een syntaxis die eigen is aan [sqlalchemy], maar die lijkt op de syntaxis van SQL;
- elke kolom wordt beschreven met een instantie van de klasse [Column] van [sqlalchemy];
- de eerste parameter is de naam van de kolom;
- de tweede parameter is het type;
- de volgende parameters zijn benoemde parameters:
- regel 17: [primary_key=True] om aan te geven dat de kolom [id] de primaire sleutel is van de tabel [personnes];
- regel 18: [nullable=False] om aan te geven dat een kolom verplicht een waarde moet hebben wanneer een rij in de tabel wordt ingevoegd;
- regel 21: ten slotte maakt de klasse [UniqueConstraint] het mogelijk om een uniekheidsbeperking te beschrijven. Hier wordt aangegeven dat de kolommen (naam, voornaam) uniek moeten zijn in de tabel. Met de eigenschap met de naam [name] kan een naam aan deze beperking worden gegeven. Hier moet een onderscheid worden gemaakt tussen twee gevallen:
- we beschrijven een bestaande tabel. Dan moeten we de naam van de beperking opzoeken in de eigenschappen van de tabel (phpMyAdmin of pgAdmin);
- je beschrijft een tabel die je gaat aanmaken. Dan voer je de gewenste naam in;
- regels 23-25: we maken een persoon aan met de naam [personne1] en geven het bijbehorende woordenboek weer: [__dict__]. Hier krijgen we dan:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- regels 27-33: we voeren een mapping uit, d.w.z. we leggen een koppeling aan tussen de klasse [Personne] en de tabel [personnes]. Dit is in wezen een koppeling [propriétés de la classe colonnes de la table]. De functie [mapper] accepteert hier drie parameters:
- regel 28: de eerste parameter is de naam van de klasse waarvoor de mapping wordt uitgevoerd;
- regel 28: de tweede parameter is de tabel waaraan deze wordt gekoppeld. Dit is het object [Table] dat op regel 16 is aangemaakt;
- regel 28: de derde parameter is hier een parameter met de naam [properties]. Dit is een woordenboek waarin de sleutels de eigenschappen van de toegewezen klasse zijn en de waarden de kolommen van de toegewezen tabel. Om kolom X van de tabel [personnes_table] aan te duiden, schrijven we [personnes_table.c.X];
- regels 35-36: de persoon [personne1] wordt opnieuw weergegeven nadat de mapping is uitgevoerd. We zien dat er niets is veranderd:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- regels 37-39: er wordt een nieuwe persoon aangemaakt, [personne2], en deze wordt weergegeven. We krijgen dan de volgende weergave:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
We zien dat het woordenboek [__dict__] ingrijpend is gewijzigd:
- (vervolg)
- er verschijnt een nieuwe eigenschap [_sa_instance_state]. We zien dat dit een object is van ORM [sqlalchemy];
- de andere eigenschappen zijn ontdaan van hun voorvoegsel dat aangaf tot welke klasse ze behoorden;
We kunnen dus concluderen dat de mappingbewerking in de regels 27-33 de klasse [Personne] heeft gewijzigd.
Wanneer we de status van een object [Personne] willen weergeven, hebben we de eigenschap [_sa_instance_state] doorgaans niet nodig. Deze is namelijk alleen bedoeld voor de interne werking van [sqlalchemy] en is voor ons over het algemeen niet van belang. Daarom hebben we in het script [config] het volgende geschreven:
# configuratie van de klassen
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
19.2.3. Het script [main]
Het script [main] gaat de tabel [personnes] van de database MySQL [dbpersonnes] bewerken door te communiceren met [sqlalchemy]. Om het vervolg te begrijpen, moet je de hier gebruikte architectuur in gedachten houden:

Als [Database1] de basis [dbpersonnes] is, zien we dat de koppeling tussen het script en deze basis via twee entiteiten verloopt:
- de Python-connector naar SGBD MySQL;
- de SGBD MySQL;
Het script [main] communiceert met ORM, dat vervolgens communiceert met de Python-connector. Het script ORM communiceert met deze connector met behulp van de tools die worden beschreven in de paragrafen |MySQL| en |PostgreSQL|, met name door SQL-opdrachten te verzenden. Het script [main] maakt geen gebruik van SQL-opdrachten. Het zal gebruikmaken van de API (Application Programming Interface) van de ORM, die bestaat uit klassen en interfaces.
Het script [main] ziet er als volgt uit:
# we configureren de applicatie
import config
config = config.configure()
# imports
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
# verbindingsstring naar een database MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadata
metadata = MetaData()
# de tabel
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')
)
# de mapping
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'voornaam': personnes_table.c.prenom,
'achternaam': personnes_table.c.nom,
'leeftijd': personnes_table.c.age
})
# de sessiefactory
Session = sessionmaker()
Session.configure(bind=engine)
session = None
try:
# een sessie
session = Session()
# verwijdering van de tabel [personnes]
session.execute("drop table if exists personnes")
# de tabel opnieuw aanmaken op basis van de mapping
metadata.create_all(engine)
# een invoeging
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
# een query
personnes = session.query(Personne).all()
# weergave
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# twee andere invoegingen, waarvan de tweede mislukt vanwege de unieke combinatie (voornaam, achternaam)
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}))
# een query
personnes = session.query(Personne).all()
# weergave
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# sessie valideren
session.commit()
except (InterfaceError, IntegrityError) as erreur:
# weergave
print(f"L'erreur suivante s'est produite : {erreur}")
# de laatste sessie annuleren
if session:
print("rollback...")
session.rollback()
finally:
# de sessiebronnen worden vrijgegeven
if session:
session.close()
Opmerkingen
- regels 1-4: de applicatie wordt geconfigureerd;
- regels 7-9: er wordt een hele reeks klassen en interfaces geïmporteerd uit de bibliotheek [sqlalchemy];
- regel 11: de klasse [Personne] wordt geïmporteerd;
- regel 14: de verbindingsstring naar de database. Hierin wordt gespecificeerd:
- de gebruikte SGBD (mysql);
- de gebruikte Python-connector (mysql.connector zonder de punt);
- de gebruiker die verbinding maakt (admpersonnes);
- zijn wachtwoord (nobody);
- de machine waarop de SGBD zich bevindt (localhost = de machine waarop het script wordt uitgevoerd);
- de naam van de database (dbpersonnes);
Met deze gegevens kan [sqlalchemy] verbinding maken met de database. Let op: de gebruikte Python-connector moet al geïnstalleerd zijn. [sqlalchemy] doet dit niet.
- regels 19-26: beschrijving van de tabel [personnes];
- regels 28-34: mapping tussen de klasse [Personne] en de tabel [personnes];
- regels 36-38: de meeste bewerkingen [sqlalchemy] vinden plaats binnen een sessie. Het begrip sessie [sqlalchemy] komt dicht in de buurt van dat van transactie SQL. Sessies worden aangemaakt op basis van de klasse [Session], die wordt geretourneerd door de functie [sessionmaker] in regel 37;
- regel 38: de klasse [Session] wordt gekoppeld aan de database [dbpersonnes] via de verbindingsstring in regel 14;
- regel 43: er wordt een sessie aangemaakt. Zoals gezegd kan een sessie worden vergeleken met een transactie;
- regels 45-46: met de methode [Session.execute] kan een opdracht SQL worden uitgevoerd. Dit komt niet vaak voor, aangezien eerder is vermeld dat met ORM de taal SQL kan worden vermeden;
- regels 48-49: met de methode [metadata.create_all] kunnen alle tabellen worden aangemaakt die gebruikmaken van de instantie [MetaData] uit regel 17. We hebben er maar één: de tabel [personnes], gedefinieerd in de regels 20-26. [sqlalchemy] gebruikt de informatie uit deze regels om de tabel aan te maken. Dit is een eerste voordeel van ORM: het verbergt de specifieke kenmerken van de SGBD-opdrachten. De volgorde van SQL en [create] kan namelijk sterk verschillen van de ene SGBD tot de andere vanwege de gegeven typen van de kolommen. Er is geen standaardisatie van de gegevenstypen doorgevoerd. Daardoor varieert de volgorde van record naar record. Hier, dankzij:
- beschrijven we op unieke wijze de tabel die we willen;
- [sqlalchemy] zorgt ervoor dat het de juiste [create] genereert voor de SGBD die het voor zich heeft;
- regel 52: we voegen een object [Personne] toe aan de sessie. Dit voegt het niet automatisch toe aan de database. Een ORM volgt namelijk zijn eigen regels om te synchroniseren met de database. Het zal altijd proberen het aantal query’s dat het uitvoert te optimaliseren. Laten we een voorbeeld nemen. Het script voegt (add) twee personen (persoon1, persoon2) toe aan de sessie en voert vervolgens een query uit: het wil alle personen zien die in de tabel staan. [sqlalchemy] kan als volgt te werk gaan:
- het toevoegen van [personne1] kan in het geheugen gebeuren. Het is voorlopig niet nodig om dit in de database op te nemen;
- hetzelfde geldt voor [personne2];
- vervolgens komt de query van het type [select]. Hiervoor moeten alle rijen uit de tabel [personnes] worden opgehaald. [sqlalchemy] zal dan [personne1, personne2] in de database opslaan en vervolgens de query uitvoeren;
[sqlalchemy] voert op deze manier optimalisaties uit die voor de ontwikkelaar onzichtbaar zijn.
- regel 56: om een query van het type [select] uit te voeren (ik wil … zien), gebruiken we de methode [Session.query]. De parameter van de methode [query] is de klasse die aan de opgevraagde tabel is gekoppeld. Deze methode retourneert een type [Query]. De methode [Query.all] vraagt alle objecten van het type [Personne] op uit de sessie. Hieraan worden alle rijen uit de tabel [personnes] teruggestuurd, elk in de vorm van een object van het type [Personne]. Hiervoor maakt [sqlalchemy] gebruik van de mapping die is gemaakt tussen de klasse [Personne] en de tabel [personnes]. Het resultaat van regel 56 is een lijst met [Personne]-objecten;
- regels 58-61: de elementen van de lijst [personnes] worden weergegeven. Omdat de klasse [Personne] is afgeleid van de klasse [BaseEntity], is de methode [Personne.__str__] die hier impliciet in regel 61 wordt gebruikt, in feite de methode [BaseEntity.__str__], die de tekenreeks jSON van het aanroepende object retourneert. Deze tekenreeks is de tekenreeks jSON uit het woordenboek [Personne.asdict] (zie |BaseEntity|). We hebben gezegd dat we na de mapping de eigenschap [_sa_instance_state] in elk object [Personne] zouden aantreffen. De waarde van deze eigenschap is echter geen type [BaseEntity]. Deze moet dus worden uitgesloten uit het woordenboek van de klasse [Personne], anders loopt de weergave vast. Dit is gedaan in het script [config];
- regels 63-65: er worden twee andere personen toegevoegd met dezelfde voor- en achternaam. Er geldt echter een uniekheidsbeperking op de vereniging van deze twee kolommen. Er zou dus een fout moeten optreden. Dat is wat we willen zien;
- regels 67-68: we vragen opnieuw de lijst van alle personen uit de database op;
- regels 70-73: en deze worden weergegeven;
- regels 75-76: de sessie wordt ‘vastgelegd’ (commit). Zoals de naam al aangeeft, wordt de onderliggende transactie vastgelegd;
- tijdens de uitvoering zullen we zien dat de regels 67-76 niet worden uitgevoerd vanwege de uitzondering die door regel 65 wordt gegenereerd. We gaan dan naar de regels 78-84 om de uitzondering af te handelen;
- regel 78: de uitzondering [InterfaceError] treedt op als [sqlalchemy] geen verbinding kan maken met de database [dbpersonnes]. De uitzondering [IntegrityError] treedt op in regel 65;
- regel 80: de fout wordt weergegeven;
- regels 82-84: als de sessie bestaat, wordt deze geannuleerd. Dit komt neer op het annuleren van de onderliggende transactie;
- regels 85-88: in alle gevallen, of er nu een fout is of niet, wordt de sessie gesloten om bronnen vrij te maken;
De resultaten van de uitvoering zijn als volgt:
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
- regels 2-3: de lijst met personen na de eerste invoer;
- regel 5: de uitzondering [IntegrityError] die optrad toen twee personen met dezelfde voor- en achternaam werden toegevoegd;
- regels 6-7: let op de opdracht SQL die is mislukt. Dit is een geconfigureerde opdracht INSERT: [sqlalchemy] heeft de twee personen met één enkele opdracht INSERT ingevoegd. Hieruit blijkt dat hij heeft geprobeerd de verzonden opdrachten SQL te optimaliseren;
Laten we nu met phpMyAdmin de inhoud van de tabel [personnes] bekijken:

In [6] zien we dat de tabel leeg is. Zelfs de eerste persoon die het script in de sessie had geplaatst, is er niet. Dit komt doordat de sessie plaatsvond binnen een transactie en deze transactie is ongedaan gemaakt in de clausule [except] van het script [main].
Laten we nu de volgende wijziging aanbrengen in [main]:
# een invoeging
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
Nadat we op regel 2 een persoon hebben toegevoegd, verwijderen we het commentaar op regel 3. De bewerking [session.commit] valideert de onderliggende transactie en er wordt een nieuwe transactie gestart. Na uitvoering is de inhoud van de tabel [personnes] als volgt:

In [6] zien we dat de eerste invoer behouden is gebleven. Dit komt doordat deze binnen transactie 1 is uitgevoerd en de daaropvolgende fout binnen transactie 2 is opgetreden.
19.3. Scripts 02: de mappings van [sqlalchemy]

De 02-scripts zijn een variant op de 01-scripts. We proberen zoveel mogelijk configuraties in [config.py] uit te voeren. We configureren daar nu de [sqlalchemy]-omgeving van de applicatie:
def configure():
# absoluut pad als referentie voor de relatieve paden van de configuratie
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute paden van de afhankelijkheden
absolute_dependencies = [
# BaseEntity, MyException, Personne, Utils
f"{root_dir}/classes/02/entities",
]
# het syspath wordt ingesteld
from myutils import set_syspath
set_syspath(absolute_dependencies)
# imports
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper, sessionmaker
# koppeling naar een database MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadata
metadata = MetaData()
# de tabel
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')
)
# de mapping
from Personne import Personne
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'voornaam': personnes_table.c.prenom,
'achternaam': personnes_table.c.nom,
'leeftijd': personnes_table.c.age
})
# de sessiefactory
Session = sessionmaker()
Session.configure(bind=engine)
# deze gegevens worden in de configuratie opgenomen
config = {}
config["Session"] = Session
config["metadata"] = metadata
config["engine"] = engine
config["personnes_table"] = personnes_table
# configuratie van de klassen
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# we voeren de configuratie uit
return config
Opmerkingen
- regels 2-12: configuratie van het Python-pad;
- regels 14-45: we configureren de omgeving [sqlalchemy];
- regels 47-52: de omgeving [sqlalchemy] wordt toegevoegd aan het configuratiedictionary;
- regels 54-56: de klasse [Personne] wordt geconfigureerd;
Met deze configuratie ziet het script [main] er als volgt uit:
# we configureren de applicatie
import config
config = config.configure()
# het syspath is geconfigureerd – we voeren de imports uit
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError
from Personne import Personne
session = None
try:
# een sessie
session = config["Session"]()
# verwijdering van de tabel [personnes]
session.execute("drop table if exists personnes")
# de tabel wordt opnieuw aangemaakt op basis van de mapping
config["metadata"].create_all(config["engine"])
# twee invoegingen
session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
session.add(personne)
# bevestiging van de twee invoegingen
session.commit()
# één query
personnes = session.query(Personne).all()
# weergave
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# twee andere invoegingen, waarvan de tweede mislukt
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
# één query
personnes = session.query(Personne).all()
# weergave
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# sessie valideren
session.commit()
except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
# weergave
print(f"L'erreur suivante s'est produite : {erreur}")
# annulering van de laatste sessie
if session:
print("rollback...")
session.rollback()
finally:
# weergave
print("Travail terminé...")
# sessiebronnen vrijgeven
if session:
session.close()
De resultaten van de uitvoering zijn als volgt:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/02/main.py
Liste des personnes-----------
{"âge": 10, "nom": "y", "prénom": "x", "id": 1}
{"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
[SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
[parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Travail terminé...
Process finished with exit code 0
In phpMyAdmin is de tabel [personnes] als volgt gewijzigd:

Laten we nu eens kijken naar de tabel [personnes] die is gegenereerd door [sqlalchemy]:

- in [6], de typen die voor de verschillende kolommen worden gebruikt;
- in [7] zien we dat de kolom [id] het attribuut [AUTO_INCREMENT] heeft. Dit betekent dat bij het invoegen van een rij in de tabel, als deze rij geen waarde heeft voor de kolom [id], deze door MySQL op incrementele wijze wordt gegenereerd: 1, 2, 3, … Dankzij deze eigenschap hoeven we ons geen zorgen te maken over de waarde van de primaire sleutel wanneer we een rij in de tabel invoegen: we laten MySQL deze genereren;
- in [8] zien we dat de kolom [id] de primaire sleutel is;
- in [9] zien we de uniekheidsbeperking op de velden [nom, prenom];
19.4. Scripts 03: bewerking van entiteiten in de sessie [sqlalchemy]

Het configuratiebestand [config] is hetzelfde als in het vorige voorbeeld. In het script [main] voeren we de standaardbewerkingen [INSERT, UPDATE, DELETE, SELECT] uit op de tabel [personnes] met behulp van de methoden van [sqlalchemy]:
# de applicatie wordt geconfigureerd
import config
config = config.configure()
# imports
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne
# de inhoud van de tabel weergeven [personnes]
def affiche_table(session: Session):
print("----------------")
# een query
personnes = session.query(Personne).all()
# weergave
affiche_personnes(personnes)
# geeft een lijst met personen weer
def affiche_personnes(personnes: list):
print("----------------")
# weergave
for personne in personnes:
print(personne)
# hoofdpagina ---------------------------
session = None
try:
# een sessie
session = config["Session"]()
# verwijdering van de tabel [personnes]
# checkfirst=True: controleert eerst of de tabel bestaat
config["personnes_table"].drop(config["engine"], checkfirst=True)
# de tabel opnieuw aanmaken op basis van de mapping
config["metadata"].create_all(config["engine"])
# invoegingen
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}))
# de inhoud van de sessie wordt weergegeven
affiche_table(session)
# lijst van personen in alfabetische volgorde op achternaam en bij gelijke achternaam in alfabetische volgorde op voornaam
personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())
# weergave
affiche_personnes(personnes)
# lijst van personen met een leeftijd binnen het interval [20,40], in aflopende volgorde van leeftijd
# vervolgens bij gelijke leeftijd in alfabetische volgorde van de achternamen en bij gelijke achternamen in alfabetische volgorde van de voornamen
personnes = session.query(Personne). \
filter(Personne.âge >= 20, Personne.âge <= 40). \
order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())
# weergave
affiche_personnes(personnes)
# toevoeging van mevrouw Bruneau
bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
session.add(bruneau)
# wijziging van haar leeftijd
bruneau.âge = 47
# lijst van personen met de achternaam Bruneau
personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()
# weergave
affiche_personnes([personne])
# verwijdering van mevrouw Bruneau
session.delete(personne)
# lijst van personen met de achternaam Bruneau
personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")
# weergave
affiche_personnes(personnes)
# sessie bevestigen
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# weergave
print(f"L'erreur suivante s'est produite : {erreur}")
# de laatste sessie annuleren
if session:
session.rollback()
finally:
# weergave
print("Travail terminé...")
# sessiebronnen vrijgeven
if session:
session.close()
Opmerkingen
- regels 20-25: de functie [affiche_personnes] geeft de elementen van een lijst met personen weer;
- regels 12-18: de functie [affiche_table] geeft de inhoud van de tabel [personnes] weer;
- regels 34-36: de tabel [personnes] wordt verwijderd. In tegenstelling tot eerdere versies wordt hier geen opdracht SQL gebruikt, maar een methode van [sqlalchemy]:
- config["personnes_table"] is het object [Table] dat de tabel [personnes] beschrijft;
- config["engine"] is de verbindingsstring voor de database [dbpersonnes];
- de parameter met de naam [checkfirst=True] zorgt ervoor dat de bewerking alleen wordt uitgevoerd als de tabel [personnes] bestaat;
- regels 38-39: de tabel [personnes] wordt opnieuw aangemaakt;
- regels 41-44: er worden drie personen aan de sessie toegevoegd. Let wel: deze worden niet noodzakelijkerwijs onmiddellijk in de tabel [personnes] ingevoerd. Dit hangt af van de prestatiegerichte strategie van [sqlalchemy];
- regels 46-47: de inhoud van de tabel [personnes] wordt weergegeven. Als de drie personen nog niet waren ingevoerd, gebeurt dat nu als gevolg van deze aanvraag;
- regels 49-50: een voorbeeld van het gebruik van de methode [order_by], waarmee de resultaten van een query in een bepaalde volgorde kunnen worden weergegeven. De syntaxis [order_by(critère1, critère2)] geeft de resultaten eerst weer op basis van het criterium [critère1] en wanneer rijen dezelfde waarde voor [critère1] hebben, worden ze vervolgens gesorteerd op basis van het criterium [critère2]. Er kunnen op deze manier meerdere criteria worden opgegeven;
- regels 55-59: introduceren het concept van een filter met de methode [filter]. De notatie [filter(critère1, critère2)] creëert een logische ET (AND) tussen de gebruikte criteria;
- regels 64-67: er wordt een nieuwe gebruiker aangemeld;
- regels 70-71: nog een voorbeeld van een gefilterde query. De functie [func.lower(param)] zet [param] om in kleine letters. Er zijn dus nog andere functies beschikbaar, aangeduid als [func.xx]. In de uitdrukking op regel 71:
- [session.query.filter] levert een lijst met objecten [Personne] op;
- [session.query.filter.first] retourneert het eerste element van deze lijst;
- regel 77: er wordt een element uit de sessie verwijderd;
- regel 86: de sessie wordt gevalideerd;
De resultaten van de uitvoering zijn als volgt:
- regels 4-6: de inhoud van de sessie;
- regels 8-10: de inhoud van de sessie in aflopende volgorde van de namen;
- regels 12-13: de inhoud van de sessie voor personen waarvan de leeftijd binnen het interval [20, 40] valt;
- regel 15: de persoon met de naam „bruneau“;
In phpMyAdmin is de inhoud van de tabel [personnes] aan het einde van de uitvoering als volgt:

19.5. Scripts 04: gebruik van een database [PostgreSQL]

Het bestand [04] is een kopie van het bestand [03]. Er wordt slechts één ding gewijzigd, namelijk de verbindingsstring in het bestand [config]:
# koppeling naar een database PostgreSQL
engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")
Deze verbindingsstring verwijst nu naar de database [dbpersonnes] van een SGBD [PostgreSQL]. Let op het gebruik van de connector [psycopg2]. Deze moet geïnstalleerd zijn.
Het uitvoeren van het script [main] levert de volgende resultaten op:
Met de tool [pgAdmin] (zie paragraaf |pgAdmin|) verkeert de tabel [personnes] in de volgende toestand:

De tabel [personnes] is gegenereerd met de volgende code SQL:

- In [4-5] zien we dat de kolom [id] de primaire sleutel is. We zien ook dat deze een standaardwaarde heeft, namelijk [mot clé DEFAULT], wat betekent dat als er een rij zonder primaire sleutel wordt ingevoegd, deze wordt gegenereerd door SGBD. Dit is een veelvoorkomende werkwijze: we laten SGBD de primaire sleutels genereren;
Deze versie 05 van de [sqlalchemy]-scripts laat goed zien hoe eenvoudig het is om van de ene SGBD naar de andere over te schakelen: het volstond om de verbindingsstring in een configuratiescript te wijzigen. Verder is er niets veranderd. Als we de kolomtypen van de bovenstaande [id, nom, prenom, age] vergelijken met die van de tabel MySQL uit voorbeeld |02|, zien we dat ze verschillen. [sqlalchemy] past ze aan aan de gebruikte SGBD. Deze flexibiliteit om zich aan te passen aan een nieuwe SGBD is reden genoeg om [sqlalchemy] of een andere ORM te gebruiken.
19.6. Scripts 05: volledig voorbeeld

Het bestudeerde voorbeeld is een herhaling van het voorbeeld dat in de paragraaf |troiscouches-v01| werd besproken. Dit voorbeeld toonde een drielaagse architectuur [ui, métier, dao] die entiteiten [Classe, Elève, Matière, Note] verwerkte. De entiteiten waren hard gecodeerd in een laag [dao]. We plaatsen ze nu in een database. We gebruiken twee SGBD: MySQL en PostgreSQL.
19.6.1. De architectuur van de applicatie
De architectuur van de applicatie ziet er als volgt uit:

- In [1-3] vinden we de lagen [ui, métier, dao] terug die al in het voorbeeld |troiscouches-v01| voorkomen. De laag [dao] communiceert nu met de laag [ORM];
- de lagen [1-5] zijn geïmplementeerd met Python-code;
19.6.2. De databases
We maken een database MySQL aan met de naam [dbecole], die eigendom is van de gebruiker [admecole] met het wachtwoord [mdpecole]. Hiervoor volgen we de procedure die wordt beschreven in de paragraaf |een database aanmaken|:


- in [1], de database [dbecole] zonder tabellen [3];
- in [7] heeft de gebruiker [admecole] alle rechten op deze database;
We doen hetzelfde met SGBD en PostgreSQL. We maken een database aan met de naam [dbecole], die eigendom is van de gebruiker [admecole] met het wachtwoord [mdpecole]. Hiervoor volgen we de procedure die wordt beschreven in de paragraaf |een database aanmaken|:

- in [1], de database [dbecole];
- in [2], de gebruiker [admecole];
- in [3-4] is de database [dbecole] eigendom van de gebruiker [admecole];
19.6.3. De entiteiten die door de applicatie worden verwerkt
In de applicatie |troiscouches v01| waren de volgende entiteiten het voorwerp van bewerking (zie |entiteiten|). Het zijn deze entiteiten die in de bovengenoemde databases zullen worden opgeslagen. We zullen deze entiteiten niet dupliceren in de nieuwe applicatie. We zullen ze ophalen waar ze al zijn gedefinieerd.
De klasse [Classe]:
# importen
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# uitgesloten attributen van de klasse-status
excluded_keys = []
# eigenschappen van de klasse
@staticmethod
def get_allowed_keys() -> list:
# id: identificatiecode van de klasse
# naam: naam van de klasse
return BaseEntity.get_allowed_keys() + ["nom"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
# setters
@nom.setter
def nom(self: object, nom: str):
# naam moet een niet-lege tekenreeks zijn
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")
De klasse [Elève]:
# imports
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# attributen die zijn uitgesloten van de status van de klasse
excluded_keys = []
# eigenschappen van de klasse
@staticmethod
def get_allowed_keys() -> list:
# id: identificatiecode van de leerling
# achternaam: achternaam van de leerling
# voornaam: voornaam van de leerling
# klas: klas van de leerling
return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
# getters
@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
# setters
@nom.setter
def nom(self: object, nom: str) -> str:
# achternaam moet een niet-lege tekenreeks zijn
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:
# voornaam moet een niet-lege tekenreeks zijn
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:
# er wordt een klasse-type verwacht
if isinstance(value, Classe):
self.__classe = value
# of een type 'dict'
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# of een json-type
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}")
De klasse [Matière]:
# imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# attributen die zijn uitgesloten van de klasse-status
excluded_keys = []
# eigenschappen van de klasse
@staticmethod
def get_allowed_keys() -> list:
# id: identificatiecode van het vak
# naam: naam van het vak
# coëfficiënt: coëfficiënt van het vak
return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
@property
def coefficient(self: object) -> float:
return self.__coefficient
# setters
@nom.setter
def nom(self: object, nom: str):
# naam moet een niet-lege tekenreeks zijn
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):
# de coëfficiënt moet een reëel getal >=0 zijn
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# fout?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
De klasse [Note]:
# imports
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# attributen die zijn uitgesloten van de klasse-status
excluded_keys = []
# eigenschappen van de klasse
@staticmethod
def get_allowed_keys() -> list:
# id: identificatiecode van de beoordeling
# waarde: het cijfer zelf
# leerling: leerling (van het type Leerling) waarop het cijfer betrekking heeft
# vak: vak (van het type Vak) waarop het cijfer betrekking heeft
# het object ‘Cijfer’ is dus het cijfer van een leerling voor een bepaald vak
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# getters
@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
# getters
@valeur.setter
def valeur(self: object, valeur: float):
# het cijfer moet een reëel getal tussen 0 en 20 zijn
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:
# er wordt een type ‘Leerling’ verwacht
if isinstance(value, Elève):
self.__élève = value
# of een type dict
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# of een json-type
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:
# er wordt een type 'Vak' verwacht
if isinstance(value, Matière):
self.__matière = value
# of een dict-type
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# of een json-type
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. Configuratie

De configuratie is opgesplitst in verschillende bestanden:
- de algemene configuratie in [config.py]: deze stelt het Python-pad van de applicatie in en instantiëert de lagen van de architectuur;
- de configuratie van [sqlalchemy] in [config_database]: deze zorgt voor de toewijzingen tussen klassen en tabellen;
- de lagen van de applicatie worden geconfigureerd in [config_layers];
Het bestand [config] ziet er als volgt uit:
def configure(config: dict) -> dict:
import os
# stap 1 ---
# het Python-pad van de applicatie instellen
# absoluut pad naar de map van dit script
script_dir = os.path.dirname(os.path.abspath(__file__))
# absoluut pad als referentie voor de relatieve paden in de configuratie
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute paden van de afhankelijkheden
absolute_dependencies = [
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# drielaags project v01
f"{root_dir}/troiscouches/v01/interfaces",
f"{root_dir}/troiscouches/v01/services",
f"{root_dir}/troiscouches/v01/entities",
# bestanden van dit project
script_dir,
f"{script_dir}/../services",
]
# syspath-update
from myutils import set_syspath
set_syspath(absolute_dependencies)
# stap 2 ------
# databaseconfiguratie
import config_database
config = config_database.configure(config)
# stap 3 ------
# instantiëren van de applicatielagen
import config_layers
config = config_layers.configure(config)
# de configuratie wordt doorgegeven
return config
- regels 4-27: samenstelling van het Python-pad van de applicatie;
- regels 29-32: configuratie van [sqlalchemy];
- regels 34-37: configuratie van de lagen van de applicatie;
Het bestand [config_database] ziet er als volgt uit:
def configure(config: dict) -> dict:
# config['sgbd'] is de naam van de gebruikte SGBD
# MySQL: MySQL
# pgres: PostgreSQL
# SQLAlchemy-configuratie
from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine
from sqlalchemy.orm import mapper, relationship, sessionmaker
# verbindingsstrings voor de gebruikte databases
engines = {
'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
}
# verbindingsstring voor de gebruikte database
engine = create_engine(engines[config['sgbd']])
# metadata
metadata = MetaData()
# de tabellen van de database
tables = {}
# de toegewezen klassen
from Classe import Classe
from Elève import Elève
from Note import Note
from Matière import Matière
# de klassentabel
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,
'naam': classes_table.c.nom
})
# de tabel met leerlingen
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),
# een leerling behoort tot een klas
Column('classe_id', Integer, ForeignKey('classes.id')),
)
# toewijzing
mapper(Elève, tables['élèves'], properties={
'id': élèves_table.c.id,
'achternaam': élèves_table.c.nom,
'voornaam': élèves_table.c.prénom,
'klas': relationship(Klas, backref="leerlingen", lazy="select")
})
# de inhoudsopgave
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)
)
# toewijzing
mapper(Matière, tables['matières'], properties={
'id': matières_table.c.id,
'naam': matières_table.c.nom,
"coefficient": matières_table.c.coefficient
})
# de cijfertabel
tables['notes'] = notes_table = \
Table("notes", metadata,
Column('id', Integer, primary_key=True),
Column('valeur', Float, nullable=False),
# een cijfer is dat van een leerling
Column('élève_id', Integer, ForeignKey('élèves.id')),
# een cijfer is dat van een vak
Column('matière_id', Integer, ForeignKey('matières.id')),
)
# toewijzing
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'waarde': notes_table.c.valeur,
'leerling': relationship(Leerling, backref="notes", lazy="select"),
'vak': relationship(Vak, backref="cijfers", lazy="select")
})
# configuratie van entiteiten [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']
# de sessiefactory
Session = sessionmaker()
Session.configure(bind=engine)
# een sessie
session = Session()
# bepaalde informatie wordt opgeslagen in het configuratiedictionary
config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
# de configuratie wordt teruggegeven
return config
Opmerkingen
- regels 1-4: de functie [configure] ontvangt een woordenboek als parameter. Alleen de sleutel [sgbd] wordt gebruikt. Deze is gelijk aan [mysql] als de database een MySQL-database is, en aan [pgres] als de database een PostgreSQL-database is;
- regels 6-9: elementen uit [sqlalchemy] worden geïmporteerd. Het script [config_database] zorgt voor de koppelingen tussen de tabellen van de database [dbecole] en de entiteiten van [Classes, Elève, Matière, Note]. In de tabel zijn de gegevens van de entiteit ingekapseld in een rij. In de Python-code zijn ze verpakt in een object. Vandaar de naam ORM (Object Relational Mapper): het script ORM legt een koppeling (mapping) tussen de rijen van een relationele database en objecten. In deze toepassing hebben we vier entiteiten [Classe, Elève, Matière, Note] die zullen worden gekoppeld aan vier tabellen [classes, élèves, matières, notes]. Let op: de namen van de tabellen mogen accenten bevatten;
- regels 11-17: de verbindingsstring naar de gebruikte database. Deze is afhankelijk van het element config[‘sgbd’];
- regels 24-28: de entiteiten van de applicatie die zullen worden gemapt naar [sqlalchemy]. Wanneer deze regels worden uitgevoerd, is het Python-pad al ingesteld door het script [config];
- regels 30-40: de mapping tussen de entiteit [Classe] en de tabel [classes];
- regels 30-35: de tabel [classes] wordt gedefinieerd met de klasse [Table] van [sqlalchemy]. We geven aan dat deze tabel twee kolommen heeft:
- de kolom [id], die de primaire sleutel is en het klassennummer bevat, regel 33;
- de kolom [nom], die de naam van de klasse bevat, regel 34;
- regels 31-32: merk op dat de syntaxis x=y=z in Python is toegestaan: de waarde van z wordt toegewezen aan y en vervolgens de waarde van y aan x;
- regels 37-40: hier worden de overeenkomsten weergegeven tussen de kolommen van de tabel [classes] en de eigenschappen van de entiteit [Classe];
- regels 42-57: de mapping tussen de entiteit [Elève] en de tabel [élèves];
- regels 51-57: de tabel [élèves] wordt gedefinieerd met de klasse [Table] van [sqlalchemy]. We geven aan dat deze tabel vier kolommen heeft:
- de kolom [id], die de primaire sleutel is en het nummer van de leerling bevat, regel 45;
- de kolom [nom], die de achternaam van de leerling bevat, regel 46;
- de kolom [prénom], die de voornaam van de leerling bevat, regel 47. Let op: een kolomnaam mag accenten bevatten;
- regel 49, de kolom [classe_id], die het nummer van de klas bevat waartoe de leerling behoort. Dit wordt een vreemde sleutel genoemd. [élèves.classe_id] is een vreemde sleutel (ForeignKey) op de kolom [classes.id]. Dit betekent dat de waarde van [élèves.classe_id] moet voorkomen in de kolom [classes.id];
- regels 51-57: hier worden de overeenkomsten weergegeven tussen de kolommen van de tabel [élèves] en de eigenschappen van de entiteit [Elève]:
- de regels 53-55 zijn eenvoudig te begrijpen;
- regel 56 is wat lastiger: hierin wordt de waarde van de eigenschap [Elève.classe] gedefinieerd als berekend door de vreemde-sleutelrelatie die de tabellen [élèves] en [classes] met elkaar verbindt. De parameters van de functie [relationship] zijn als volgt:
- [Classe]: dit is de naam van de entiteit waarmee de entiteit [Elève] een vreemde-sleutelrelatie heeft. Dit moet in de tabel [élèves] tot uiting komen door de aanwezigheid van een vreemde sleutel naar de tabel [classes]. We weten dat deze bestaat;
- [backref="élèves"]: de naam van een eigenschap die aan de entiteit [Classe] zal worden toegevoegd. [Classe.élèves] zal de lijst zijn van alle leerlingen van de klas. Deze eigenschap mag nog niet bestaan. Als deze al bestaat, moet hier gewoon een andere naam worden gekozen voor [backref]. De ontwikkelaar hoeft deze eigenschap niet te beheren. Dat doet [sqlalchemy]. Hij hoeft alleen te weten dat deze eigenschap bestaat, toegevoegd door [sqlalchemy], en dat hij deze in zijn code kan gebruiken;
- [lazy=’select’]: dit betekent dat ORM niet onmiddellijk een waarde aan de eigenschap [Elève.classe] hoeft toe te kennen. Hij hoeft de waarde ervan pas op te zoeken wanneer de code daar expliciet om vraagt. Dus:
- als de code de lijst met alle leerlingen opvraagt, worden deze wel opgehaald, maar wordt hun eigenschap [classe] niet berekend;
- iets later richt de code zich op een specifieke leerling [e] en verwijst naar diens klas [e.classe]. Deze verwijzing dwingt [sqlalchemy] er vervolgens toe een databasequery uit te voeren om de klas van de leerling op te halen, en dit op een voor de ontwikkelaar transparante manier;
- ook het toevoegen van [lazy=’select’] is bedoeld om onnodige databasequery’s te voorkomen;
- regel 56: wanneer ORM een rij uit de tabel [élèves] ophaalt, haalt het de informatie uit [id, nom, prénom, classe_id] op. Op basis daarvan moet het een object Leerling(id, achternaam, voornaam, klas) samenstellen. Voor de eigenschappen [id, nom, prénom] levert dit geen problemen op. Voor de eigenschap [classe] is het ingewikkelder. De waarde ervan is een objectverwijzing van het type [Classe]. Maar het object ORM bevat alleen de informatie [élèves.classe_id]. Aangezien [élèves.classe_id] een vreemde sleutel is op de kolom [classes.id], wordt hier aangegeven dat deze relatie moet worden gebruikt om in de tabel [classes] de rij met id=[élèves.classe_id] (die onvermijdelijk bestaat) en op basis van deze rij het object [Classe] aan te maken dat wordt verwacht door de eigenschap [Elève.classe];
- regels 59-71: de mapping tussen de entiteit [Matière] en de tabel [matières];
- regels 59-65: definitie van de tabel [sqlalchemy] met de naam [matières];
- regels 66-71: hier worden de overeenkomsten tussen de kolommen van de tabel [matières] en de eigenschappen van de entiteit [Matière] opgesomd. Hier zijn geen problemen;
- regels 73-90: de mapping tussen de entiteit [Note] en de tabel [notes];
- regels 73-82: definitie van de tabel [sqlalchemy], genaamd [notes]. Deze heeft twee vreemde sleutels:
- regel 79: de kolom [notes.élève_id] haalt zijn waarden uit de kolom [élèves.id]]. Deze vreemde sleutel geeft aan dat een cijfer het cijfer van een specifieke leerling is;
- regel 81: de kolom [notes.matière_id] haalt zijn waarden uit de kolom [matières.id]. Deze externe sleutel geeft aan dat een cijfer een cijfer is voor een bepaald vak;
- regels 84-90: de koppeling tussen de entiteit [Note] en de tabel [notes]:
- regel 88: de eigenschap [Note.élève] moet een instantie van het type [Elève] als waarde hebben. De ORM bevat in de rij van de tabel [notes] alleen de kolom [notes.élève_id], die verwijst naar de kolom [élèves.id]. Hier wordt aangegeven dat deze vreemde-sleutelrelatie moet worden gebruikt om de instantie [Elève] op te zoeken waarvan de beoordeling bekend is. Bovendien zal [relationship(Elève, backref="notes", …)] de nieuwe eigenschap [Elève.notes] aanmaken, die de lijst met cijfers van de leerling zal bevatten. Deze eigenschap mag nog niet bestaan in de klasse [Elève];
- regel 89: de eigenschap [Note.matière] moet als waarde een instantie van het type [Matière] hebben. De ORM bevat in de rij van de tabel [notes] alleen de kolom [notes.matière_id], die verwijst naar de kolom [matières.id]. Hier wordt aangegeven dat deze vreemde-sleutelrelatie moet worden gebruikt om de instantie [Matière] op te zoeken waarvan de aantekening bekend is. Bovendien zal [relationship(Matière, backref="notes", …)] de nieuwe eigenschap [Matière.notes] aanmaken, die de lijst met cijfers voor het vak zal bevatten. Deze eigenschap mag nog niet bestaan in de klasse [Matière];
- regels 92-96: voor elke entiteit die is afgeleid van [BaseEntity] wordt de lijst met eigenschappen gedefinieerd die moeten worden uitgesloten van het eigenschappenwoordenboek van de entiteit (BaseEntity.asdict). We hebben gezien dat [sqlalchemy] de eigenschap [_sa_instance_state] toevoegde aan alle toegewezen entiteiten. Deze willen we niet in het eigenschappenwoordenboek hebben. Daarnaast hebben we gezien dat de voorgaande toewijzingen nieuwe eigenschappen aan de entiteiten hadden toegevoegd:
- [Elève.notes]: alle cijfers van de leerling;
- [Classe.élèves]: alle leerlingen van de klas;
- [Matière.notes]: alle cijfers van het vak;
Over het algemeen willen we niet dat deze eigenschappen aan de toestand van de entiteit worden toegevoegd. Het berekenen van hun waarde kost namelijk SQL en deze waarde is vaak nutteloos. Als we dus de leerling met de naam ‘X’ ophalen:
- (vervolg)
- zal de ORM een entiteit [Elève(id, nom, prénom, classe, notes)] opleveren. Vanwege [lazy=’select’] zijn de eigenschappen [classe, notes] die gekoppeld zijn aan vreemde sleutels in de database niet berekend;
- als ik nu de reeks jSON van deze leerling weergeef, weten we dat dit de reeks jSON uit het woordenboek [asdict] van de entiteit zal zijn. Als de eigenschappen [classe] en [notes] daarin voorkomen, zal [sqlalchemy] gedwongen zijn om SQL-query's uit te voeren om hun waarden te berekenen. Dat is kostbaar. Als deze query's kunnen worden vermeden, heeft dat de voorkeur;
- hier hebben we alle eigenschappen die aan een vreemde sleutel zijn gekoppeld, uitgesloten;
- regels 98-100: instantiëren en configureren van een [Session factory] (factory = productiefabriek). Het object [Session] wordt gebruikt om [sqlalchemy]-sessies te maken die aan transacties zijn gekoppeld;
- regels 102-103: aanmaken van een sqlalchemy-sessie];
- regel 106: bepaalde elementen van de configuratie [sqlalchemy] worden opgeslagen in het globale configuratiedictionary van de applicatie;
- regel 109: dit woordenboek wordt teruggegeven;
Het bestand [config_layers] configureert de lagen van de applicatie:
def configure(config: dict) -> dict:
# instantiatie van de laag [dao]
from DatabaseDao import DatabaseDao
dao = DatabaseDao(config)
# instantie van de laag [métier]
from Métier import Métier
métier = Métier(dao)
# instantie van de laag [ui]
from Console import Console
ui = Console(métier)
# de lagen worden in de configuratie opgenomen
config['dao'] = dao
config['métier'] = métier
config['ui'] = ui
# de configuratie wordt teruggezet
return config
- regel 1: de functie [configure] ontvangt het woordenboek van de algemene applicatieconfiguratie;
- regels 2-12: de lagen van de applicatie worden geïnstantieerd;
- regels 15-17: de verwijzingen naar de lagen worden in de algemene configuratie geplaatst;
- regel 20: de nieuwe configuratie wordt teruggegeven;
19.6.5. De laag [dao] - 1

Hierbij moet worden opgemerkt dat de laag [dao] [3] communiceert met deORM [sqlalchemy] [4], die is geconfigureerd zoals beschreven in de vorige paragraaf. Van de drie lagen [ui, métier, dao] van de applicatie |troiscouches v01| hoeft alleen de laag [dao] te worden herschreven. De lagen [ui, métier] blijven behouden.
De implementatie van de laag [dao] is in de map [services] geplaatst:

[InterfaceDatabaseDao] is de interface van de laag [dao]:
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
class InterfaceDatabaseDao(InterfaceDao, ABC):
# initialisatie van de database
@abstractmethod
def init_database(self, data: dict):
pass
- regel 6: de interface [InterfaceDatabaseDao] is zowel afgeleid van de klasse [ABC] (om een abstracte klasse te zijn) als van de interface [InterfaceDao] van het project |troiscouches v01|;
- regels 8-11: de methode [init_database] wordt toegevoegd aan de methoden die zijn overgenomen van [InterfaceDao]. Deze methode heeft als taak de database te initialiseren met de gegevens uit het woordenboek [data], dat in regel 10 als parameter wordt doorgegeven;
Ter herinnering: de interface [InterfaceDao] zag er als volgt uit:
# imports
from abc import ABC, abstractmethod
# DAO-interface
from Elève import Elève
class InterfaceDao(ABC):
# lijst met klassen
@abstractmethod
def get_classes(self: object) -> list:
pass
# lijst met leerlingen
@abstractmethod
def get_élèves(self: object) -> list:
pass
# lijst met vakken
@abstractmethod
def get_matières(self: object) -> list:
pass
# lijst met cijfers
@abstractmethod
def get_notes(self: object) -> list:
pass
# lijst met cijfers van een leerling
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# een leerling zoeken op zijn of haar id
@abstractmethod
def get_élève_by_id(self: object, élève_id: int) -> Elève:
pass
De implementatie van de laag [dao] is als volgt:
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note
class DatabaseDao(InterfaceDatabaseDao):
def __init__(self, config: dict):
# database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
…
- regel 11: de klasse [DatabaseDao] implementeert de interface [InterfaceDatabaseDao];
- regels 13-16: de constructor van de klasse. Deze ontvangt als parameter het woordenboek met de configuratie van de applicatie;
- regel 15: de configuratie [sqlalchemy] wordt opgeslagen;
- regel 16: de sessie [sqlalchemy] wordt opgeslagen, waarmee de database zal worden beheerd;
- regel 18: de methode [init_database] initialiseert de database met het woordenboek [data];
Het woordenboek [data] wordt geïmplementeerd door het volgende script [data.py]:
def configure():
from Classe import Classe
from Elève import Elève
from Matière import Matière
from Note import Note
# de klassen worden geïnstantieerd
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
classes = [classe1, classe2]
# de vakken
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]
# de leerlingen
é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]
# de cijfers van de leerlingen voor de verschillende vakken
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]
# we voegen alles samen
data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
# de gegevens worden weergegeven
return data
- regel 34: het woordenboek dat wordt doorgegeven aan de methode [init_database]. Dit woordenboek bestaat uit de volgende sleutels (regel 32):
- [élèves]: de lijst met leerlingen;
- [classes]: de lijst met klassen;
- [matières]: de lijst met vakken;
- [notes]: de lijst met cijfers van alle leerlingen voor alle vakken;
Laten we teruggaan naar de methode [init_database]:
def init_database(self, data: dict):
# database-instellingen
database = self.database
engine = database['engine']
metadata = database['metadata']
tables = database['tables']
try:
# bestaande tabellen verwijderen
# checkfirst=True: controleert eerst of de tabel bestaat
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)
# tabellen opnieuw aanmaken op basis van de mapping
metadata.create_all(engine)
# tabellen vullen
session = self.session
# klassen
classes = data["classes"]
for classe in classes:
session.add(classe)
# vakken
matières = data["matières"]
for matière in matières:
session.add(matière)
# leerlingen
élèves = data["élèves"]
for élève in élèves:
session.add(élève)
# cijfers
notes = data["notes"]
for note in notes:
session.add(note)
# commit
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# sessie annuleren
if session:
session.rollback()
# de uitzondering wordt doorgegeven
raise MyException(23, f"{erreur}")
- regels 3-6: we halen informatie op uit de database-configuratie;
- regels 9-14: we hebben gezien dat de configuratie [sqlalchemy] vier entiteiten aan vier tabellen [élèves, matières, classes, notes] had gekoppeld. We beginnen met het verwijderen van deze tabellen, indien ze bestaan;
- regels 16-17: we maken de vier tabellen die we zojuist hebben verwijderd opnieuw aan;
- regels 22-25: we plaatsen alle klassen in de sessie;
- regels 27-30: alle vakken worden aan de sessie toegevoegd;
- regels 32-35: we plaatsen alle leerlingen in de sessie;
- regels 37-40: we voegen alle cijfers toe aan de sessie;
- bij het toevoegen hebben we een bepaalde volgorde aangehouden. We zijn begonnen met de entiteiten die geen relaties met andere entiteiten hadden en zijn geëindigd met de entiteiten die dat wel hadden. Wanneer we dus de leerlingen aan de sessie toevoegen, bevinden de klassen waarnaar zij verwijzen zich al in de sessie;
- regel 43: de sessie [sqlalchemy] is gevalideerd. Na deze bewerking weten we zeker dat alle gegevens in de sessie zijn gesynchroniseerd met de database. Met andere woorden: ze zijn in de tabellen terechtgekomen. Dit was mogelijk dankzij de toewijzingen die zijn gemaakt in de configuratie van [sqlalchemy]. [sqlalchemy] weet hoe elke entiteit in de tabellen moet worden opgeslagen. [sqlalchemy] heeft ook de vreemde sleutels gegenereerd die de tabellen kunnen bevatten;
- regels 44-49: als er een probleem optreedt, wordt de sessie [sqlalchemy] afgebroken en wordt in regel 49 een uitzondering gegenereerd;
19.6.6. Initialisatie van de database

Het script [main_init_database] initialiseert de database met de inhoud van het script [data.py]. De code ervan is als volgt:
# er wordt gewacht op een mysql- of pgres-parameter
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()
# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})
# het syspath is geconfigureerd – de imports kunnen worden uitgevoerd
from MyException import MyException
# de gegevens die in de database moeten worden ingevoerd, worden opgehaald
import data
data = data.configure()
# de laag wordt opgehaald [dao]
dao = config["dao"]
# ----------- hoofdpagina
try:
# aanmaken en initialiseren van de databasetabellen
dao.init_database(data)
except MyException as ex:
# de foutmelding wordt weergegeven
print(f"L'erreur suivante s'est produite : {ex}")
finally:
# vrijgeven van de door de applicatie gebruikte bronnen
import shutdown
shutdown.execute(config)
# einde
print("Travail terminé...")
- regels 1-11: het script verwacht een parameter [mysql] of [pgres], afhankelijk van of men een database MySQL of PostgreSQL wil initialiseren;
- regels 13-15: de applicatie is geconfigureerd voor de als parameter doorgegeven SGBD;
- regels 20-22: de gegevens die in de database moeten worden opgeslagen, worden opgehaald;
- regel 25: de laag [dao] is al geïnstantieerd en is toegankelijk in de configuratie van de applicatie;
- regel 30: de database wordt geïnitialiseerd;
- regels 34-37: of er nu een fout is opgetreden of niet, de applicatiebronnen worden vrijgegeven met behulp van de module [shutdown];
De module [shutdown.py] ziet er als volgt uit:
def execute(config: dict):
# de door de applicatie gebruikte bronnen worden vrijgegeven
sqlalchemy_session = config['database']['session']
if sqlalchemy_session:
sqlalchemy_session.close()
De functie [shutdown.execute] sluit de sessie [sqlalchemy] af die is gebruikt om de database te initialiseren.
We maken een eerste uitvoeringsconfiguratie (zie |uitvoeringsconfiguratie|) om [main_init_database] uit te voeren met SGBD en MySQL:

De resultaten van de uitvoering van deze configuratie zijn als volgt in phpMyAdmin:



Voor SGBD en [PostgreSQL] gebruiken we de volgende uitvoeringsconfiguratie:

Bij uitvoering zijn de resultaten in [pgAdmin] als volgt:



Merk op hoe eenvoudig het was om over te schakelen naar SGBD.
19.6.7. De laag [dao] – 2
We komen terug op de klasse [DatabaseDao] die de laag [dao] implementeert. Tot nu toe hebben we alleen de implementatie van de methode [init_database] laten zien. We laten nu de implementatie van de andere methoden zien:
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note
class DatabaseDao(InterfaceDatabaseDao):
def __init__(self, config: dict):
# database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
# lijst van alle klassen
def get_classes(self: object) -> list:
# verzoek
return self.session.query(Classe).all()
# lijst van alle leerlingen
def get_élèves(self: object) -> list:
# query
return self.session.query(Elève).all()
# lijst van alle vakken
def get_matières(self: object) -> list:
# verzoek
return self.session.query(Matière).all()
# de lijst met cijfers van alle leerlingen
def get_notes(self: object) -> list:
# verzoek
return self.session.query(Note).all()
# de lijst met cijfers van een bepaalde leerling
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
# de leerling wordt opgezocht – er wordt een uitzondering gegenereerd als deze niet bestaat
# de gebruiker wordt doorgestuurd
élève = self.get_élève_by_id(élève_id)
# zijn cijfers worden opgehaald (lazy loading)
notes = élève.notes
# er wordt een woordenboek geretourneerd
return {"élève": élève, "notes": notes}
# een leerling wordt geïdentificeerd aan de hand van zijn nummer
def get_élève_by_id(self, élève_id: int) -> Elève:
# de leerling wordt opgezocht
élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
# is hij gevonden?
if élèves:
return élèves[0]
else:
raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")
# een leerling opgespoord aan de hand van zijn naam
def get_élève_by_name(self, élève_name: str) -> Elève:
# we zoeken de leerling
élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
# is hij gevonden?
if élèves:
return élèves[0]
else:
raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")
# een klas geïdentificeerd aan de hand van het nummer
def get_classe_by_id(self, classe_id: int) -> Classe:
# we zoeken de klas
classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
# hebben we hem gevonden?
if classes:
return classes[0]
else:
raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")
# een klas die wordt aangeduid met zijn naam
def get_classe_by_name(self, classe_name: str) -> Classe:
# we zoeken de klas
classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
# hebben we die gevonden?
if classes:
return classes[0]
else:
raise MyException(14, f"La classe de nom {classe_name} n'existe pas")
# een vak dat aan de hand van het nummer wordt geïdentificeerd
def get_matière_by_id(self, matière_id: int) -> Matière:
# we zoeken het vak
matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
# hebben we het gevonden?
if matières:
return matières[0]
else:
raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")
# een stof die aan de hand van de naam is geïdentificeerd
def get_matière_by_name(self, matière_name: str) -> Matière:
# we zoeken het materiaal
matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
# hebben we het gevonden?
if matières:
return matières[0]
else:
raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
- regels 21-24: de methode [get_classes] moet de lijst met klassen van de school weergeven. In regel 20 gebruiken we een query die we al eerder zijn tegengekomen;
- regels 26-39: drie andere, vergelijkbare methoden om de lijsten met leerlingen, vakken en cijfers op te halen;
- regels 51-59: de methode [get_élève_by_id] moet een leerling weergeven die wordt geïdentificeerd aan de hand van zijn nummer. Er wordt een uitzondering gegenereerd als deze niet bestaat;
- regel 54: we gebruiken een gefilterde query. We krijgen een lege lijst of een lijst met één element;
- regel 57: als de opgehaalde lijst niet leeg is, wordt het eerste element van de lijst geretourneerd;
- anders, regel 59, wordt er een uitzondering gegenereerd;
- regels 41-49: de methode [get_notes_for_élève_by_id] moet de cijfers retourneren van een leerling die wordt geïdentificeerd aan de hand van zijn nummer:
- regel 45: we gebruiken de methode [get_élève_by_id] om de entiteit Leerling van de leerling op te halen;
- regel 47: we gebruiken de eigenschap [Elève.notes], die is aangemaakt door de mapping tussen de entiteit [Note] en de tabel [notes] (zie paragraaf |SQLAlchemy-configuratie|) en die de cijfers van de leerling vertegenwoordigt;
- regel 49: er wordt een woordenboek geretourneerd;
- regels 61-109: een reeks vergelijkbare methoden waarmee het mogelijk is om:
- een leerling op naam zoeken, regels 61-69;
- een klas opzoeken, regels 71-89;
- een vak opzoeken, regels 91-109;
19.6.8. Het script [main_joined_queries]

Het script [main_joined_queries] heet zo omdat het bedoeld is om de verzoeken te belichten die impliciet door [sqlalchemy] worden gedaan om informatie uit meerdere tabellen op te halen. Deze voor de programmeur verborgen query’s worden uitgevoerd telkens wanneer een eigenschap van een entiteit in de mapping van de entiteit is gekoppeld aan de functie [relationship]. Bijvoorbeeld:
# toewijzing
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'waarde': notes_table.c.valeur,
'leerling': relationship(Leerling, backref="notes", lazy="select"),
'vak': relationship(Vak, backref="cijfers", lazy="select")
})
Hierboven de mapping tussen de entiteit [Note] en de tabel [notes]:
- regel 5: wanneer de eigenschap [élève] van een entiteit [Note] voor de eerste keer wordt opgevraagd, wordt deze opgezocht in de tabel [élèves] via een query SQL. Zolang deze eigenschap niet is opgevraagd, blijft deze ongedefinieerd (lazy load). Zodra deze is opgehaald, blijft de waarde ervan in het geheugen van de ORM opgeslagen. Wanneer er een tweede keer naar wordt verwezen, levert de ORM onmiddellijk de waarde af zonder dat er een nieuwe query SQL nodig is. Dit alles verloopt transparant voor de ontwikkelaar;
- hetzelfde geldt voor de omgekeerde eigenschap [Elève.notes] (backref), regel 5;
- hetzelfde geldt voor de eigenschap [Note.matière] en de bijbehorende omgekeerde eigenschap [Matière.notes] (backref), regel 6;
Het script [main_joined_queries] is als volgt:
# er wordt een MySQL- of PostgreSQL-parameter verwacht
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()
# de applicatie wordt geconfigureerd
import config
config = config.configure({"sgbd": sgbd})
# het syspath is geconfigureerd – we kunnen de imports uitvoeren
from MyException import MyException
# de laag [dao]
dao = config["dao"]
try:
# leerling op id
print("élève id=11 -----------")
élève = dao.get_élève_by_id(11)
print(f"élève={élève}")
# de klas van de leerling (lazy loading)
classe = élève.classe
print(f"classe de l'élève : {classe}")
# de leerlingen van dezelfde klas (lazy loading)
print("élèves dans la même classe :")
for élève in classe.élèves:
print(f"élève={élève}")
# een leerling op naam
print("élève nom='nom2' -----------")
print(f"élève={dao.get_élève_by_name('nom2')}")
# zijn/haar klas (lazy loading)
print(f"classe de l'élève : {élève.classe}")
# cijfers van een leerling
print("notes de l'élève id=11 -----------")
# eerst de leerling
élève = dao.get_élève_by_id(11)
# daarna zijn cijfers (lazy loading)
for note in élève.notes:
# het cijfer
print(f"note={note}, "
# het vak van het cijfer (lazy loading)
f"matière={note.matière}")
# de leerlingen van een klas
print("élèves de la classe nom='classe1' -----------")
# eerst de klas
classe = dao.get_classe_by_name('classe1')
# daarna de leerlingen (lazy loading)
for élève in classe.élèves:
print(élève)
# hetzelfde geldt voor [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)
# de cijfers voor een vak
print("matière de nom='matière1' -----------")
# eerst het vak
matière = dao.get_matière_by_name('matière1')
print(f"matière={matière}")
# daarna de cijfers voor dit vak (lazy loading)
print("Notes dans la matière : ")
for note in matière.notes:
print(note)
# hetzelfde geldt voor vak 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:
# de foutmelding wordt weergegeven
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# de fout wordt weergegeven
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# de bronnen worden vrijgegeven
import shutdown
shutdown.execute(config)
De opmerkingen volstaan om de code te begrijpen.
We maken een uitvoerconfiguratie aan voor MySQL:

De resultaten van de uitvoering zijn als volgt:
Om deze resultaten te begrijpen, moet men in gedachten houden dat bepaalde eigenschappen uit het entiteitenwoordenboek zijn uitgesloten (zie |configuratie|):
# configuratie van entiteiten [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']
Wanneer we dus [print(f"élève={élève}")] op regel 26 van de code schrijven, geeft regel 1 hierboven aan dat de eigenschappen van ['_sa_instance_state', 'notes', 'classe'] niet zullen worden weergegeven. Dit zien we op regel 3 van de resultaten. Alle andere eigenschappen worden wel weergegeven. Zo ontdekken we, nog steeds op regel 3, een nieuwe eigenschap [classe_id] die aanvankelijk niet bestond in de entiteit [Elève]. Deze eigenschap komt rechtstreeks overeen met de kolom [classe_id] van de tabel [élèves]. [sqlalchemy] heeft dus de volgende eigenschappen toegevoegd aan de entiteit [Elève]: [classe_id, _sa_instance_state, notes]. Het is belangrijk om hiervan op de hoogte te zijn, met name omdat deze eigenschappen nog niet mogen bestaan in de toegewezen entiteit.
De eigenschappen die uit het entiteitenwoordenboek worden uitgesloten, zijn belangrijk. Als bijvoorbeeld de eigenschappen [notes, élève] niet worden uitgesloten van de entiteit [Elève], dan zal de bewerking [print(f"élève={élève}")] deze weergeven en zal dus, zoals zojuist is uitgelegd, leiden tot impliciete SQL-query’s (lazy loading) om de waarden van deze eigenschappen op te halen. Als, zoals hier, een lijst met leerlingen wordt weergegeven, worden de impliciete bewerkingen SQL voor elke leerling uitgevoerd. Dit kan enerzijds overbodig zijn en anderzijds zeker veel uitvoeringstijd kosten.
Om het script met een basis PostgreSQL uit te voeren, maken we de volgende uitvoeringsconfiguratie aan:

De uitvoering levert dezelfde resultaten op als met MySQL.
19.6.9. Het script [main_stats_for_élève]

Het script [main_stats_for_élève] is het script dat al werd gebruikt in de applicatie [|troiscouches v01]. Het heette toen [main]. Het is een console-applicatie waarmee bepaalde indicatoren over de cijfers van een leerling kunnen worden verkregen: [moyenne pondérée, min, max, liste]. Het past in de volgende architectuur:

In deze gelaagde architectuur is alleen de laag [dao] gewijzigd tussen de applicatie |troiscouches v01| en deze applicatie. Aangezien de nieuwe laag [dao] voldoet aan de interface [InterfaceDao] van de oude laag [dao], hoeven de lagen [ui, métier] niet te worden gewijzigd. We kunnen dus de lagen blijven gebruiken die zijn gedefinieerd in de applicatie |troiscouches v01|.
Het script [main_stats_for_élève] implementeert de laag [main] uit het bovenstaande schema als volgt:
# er wordt gewacht op een mysql- of pgres-parameter
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()
# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})
# het syspath is geconfigureerd – de imports kunnen worden uitgevoerd
from MyException import MyException
# de laag [ui]
ui = config["ui"]
try:
# uitvoering van de laag [ui]
ui.run()
except MyException as ex1:
# de fout wordt weergegeven
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# de fout wordt weergegeven
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# de bronnen worden vrijgegeven
import shutdown
shutdown.execute(config)
- regel 20: er wordt een verwijzing opgehaald naar de laag [ui] in de configuratie van de applicatie;
- regel 24: de dialoog met de gebruiker wordt gestart met behulp van de enige methode van de laag [ui];
Een uitvoeringsconfiguratie voor PostgreSQL zou er als volgt uitzien:

Hier volgt een voorbeeld van een uitvoering met deze configuratie:
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