19. Utilizzo di ORM SQLALCHEMY
Il capitolo precedente ha mostrato che in alcuni casi è possibile scrivere codice indipendente da SGBD, utilizzato con la seguente architettura:

In questo capitolo utilizzeremo l’Object Relational Mapper (ORM) ORM per accedere ai SGBD in modo uniforme, indipendentemente dal SGBD utilizzato. Un ORM consente due cose:
- consente a uno script di interagire con il SGBD senza emettere comandi SQL;
- nasconde allo script le peculiarità di ciascun SGBD;
L’architettura diventa la seguente:
Lo script è ora separato dai connettori tramite l’ORM. Comunica con l’ORM tramite classi e metodi. Non esegue codice SQL. È l’ORM a farlo con i connettori a cui è collegato. Nasconde allo script le peculiarità di questi connettori. Pertanto, il codice dello script è insensibile a un cambio di connettore (quindi dell’SGBD);
La struttura gerarchica degli script esaminati sarà la seguente:

19.1. Installazione di ORM [sqlalchemy]
L’ORM [sqlalchemy] si presenta sotto forma di un pacchetto Python che deve essere installato in un terminale 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. Script 01: le basi

- in [1], gli script che verranno esaminati. Questi script utilizzeranno le classi di [2]: BaseEntity, MyException, Personne, Utils;
19.2.1. Configurazione
Il file [config] configura l’applicazione nel modo seguente:
def configure():
# root_dir
# percorso assoluto di riferimento dei percorsi relativi della configurazione
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# percorsi assoluti delle dipendenze
absolute_dependencies = [
# BaseEntity, MyException, Persona, Utili
f"{root_dir}/classes/02/entities",
]
# si imposta il syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# configurazione delle classi
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# si salva la configurazione
return {}
Commenti
- riga 8: si aggiunge al Python Path la cartella contenente le classi [BaseEntity, MyException, Personne, Utils];
- righe 12-13: si imposta il Python Path dell’applicazione;
- righe 16-17: si ricorda che la classe |BaseEntity| ha un attributo di classe denominato [excluded_keys]. Questo attributo è una lista in cui si inseriscono le proprietà della classe che non si desidera vedano apparire nel suo dizionario (funzione asdict). Qui si esclude la proprietà [_sa_instance_state] dallo stato della classe [Personne]. Vedremo presto il motivo;
19.2.2. Script [démo]
Lo script [démo] mostra un primo utilizzo di ORM [sqlalchemy]:
# si recupera la configurazione dell'applicazione
import config
config = config.configure()
# importazioni
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper
from Personne import Personne
# metadati
metadata = MetaData()
# la tabella
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')
)
# la classe Persona prima del mapping
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")
# il mapping
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nome': personnes_table.c.prenom,
'cognome': personnes_table.c.nom,
'età': personnes_table.c.age
})
# la persona 1 non è stata modificata
print(f"personne1={personne1.__dict__}")
# la classe Persona è stata modificata: è stata «arricchita»
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")
Commenti
- righe 1-4: si configura l’applicazione;
- righe 6-10: si importano i moduli necessari allo script;
- riga 13: [MetaData] è una classe di [sqlalchemy];
- righe 15-22: [Table] è una sottoclasse di [sqlalchemy]. Consente di descrivere una tabella di un database. In questa sede descriveremo la tabella [personnes] del database MySQL [dbpersonnes] trattato nel capitolo |MySQL|;
- riga 16: il primo parametro [personnes] è il nome della tabella descritta;
- riga 16: il secondo parametro [metadata] è l’istanza [MetaData] creata alla riga 13;
- righe 17-22: ciascuno dei seguenti parametri descrive una colonna della tabella con una sintassi propria di [sqlalchemy], ma simile alla sintassi di SQL;
- ogni colonna è descritta da un'istanza della classe [Column] di [sqlalchemy];
- il primo parametro è il nome della colonna;
- il secondo parametro è il suo tipo;
- i parametri successivi sono parametri denominati:
- riga 17: [primary_key=True] per indicare che la colonna [id] è la chiave primaria della tabella [personnes];
- riga 18: [nullable=False] per indicare che una colonna deve necessariamente avere un valore quando viene inserita una riga nella tabella;
- riga 21: infine, la classe [UniqueConstraint] consente di descrivere un vincolo di unicità. Qui si indica che le colonne (cognome, nome) devono essere uniche nella tabella. La proprietà denominata [name] consente di assegnare un nome a questo vincolo. In questo caso, occorre distinguere due casi:
- si descrive una tabella esistente. In tal caso, occorre cercare il nome del vincolo tra le proprietà della tabella (phpMyAdmin o pgAdmin);
- si sta descrivendo una tabella che si intende creare. In questo caso si inserisce il nome desiderato;
- righe 23-25: si crea una persona [personne1] e si visualizza il suo dizionario [__dict__]. Qui avremo:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- righe 27-33: si esegue un mapping, ovvero si crea una corrispondenza tra la classe [Personne] e la tabella [personnes]. Si tratta essenzialmente di una corrispondenza [propriétés de la classe colonnes de la table]. La funzione [mapper] accetta qui tre parametri:
- riga 28: il primo parametro è il nome della classe per la quale si esegue il mapping;
- riga 28: il secondo parametro è la tabella a cui verrà associata. Si tratta dell’oggetto [Table] creato alla riga 16;
- riga 28: il terzo parametro è qui un parametro denominato [properties]. Si tratta di un dizionario in cui le chiavi sono le proprietà della classe mappata e i valori le colonne della tabella mappata. Per indicare la colonna X della tabella [personnes_table], si scrive [personnes_table.c.X];
- righe 35-36: si visualizza nuovamente la persona [personne1] una volta effettuato il mapping. Si nota che non è cambiata:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- righe 37-39: si crea una nuova persona [personne2] e la si visualizza. Si ottiene quindi la seguente visualizzazione:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
Si nota che il dizionario [__dict__] è stato profondamente modificato:
- (continua)
- appare una nuova proprietà [_sa_instance_state]. Si nota che si tratta di un oggetto appartenente a ORM [sqlalchemy];
- le altre proprietà sono state private del prefisso che indicava a quale classe appartenessero;
Si può quindi concludere che l’operazione di mappatura delle righe 27-33 ha modificato la classe [Personne].
Quando si vorrà visualizzare lo stato di un oggetto [Personne], in genere non si vorrà la proprietà [_sa_instance_state]. Essa è infatti presente solo per il funzionamento interno di [sqlalchemy] e in genere non ci interessa. Ecco perché nello script [config] abbiamo scritto:
# configurazione delle classi
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
19.2.3. Lo script [main]
Lo script [main] gestirà la tabella [personnes] del database MySQL [dbpersonnes] interfacciandosi con [sqlalchemy]. Per comprendere il seguito, è necessario ricordare l’architettura qui utilizzata:

Se [Database1] è la base [dbpersonnes], si nota che il collegamento tra lo script e questa base passa attraverso due entità:
- il connettore Python a SGBD MySQL;
- il SGBD MySQL;
Lo script [main] comunicherà con ORM, che a sua volta comunicherà con il connettore Python. Lo script ORM comunica con questo connettore utilizzando gli strumenti descritti nei paragrafi |MySQL| e |PostgreSQL|, in particolare inviando comandi SQL. Lo script [main] non utilizzerà comandi SQL. Si baserà sull’API (Application Programming Interface) di ORM, costituita da classi e interfacce.
Lo script [main] è il seguente:
# si configura l'applicazione
import config
config = config.configure()
# importazioni
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
# stringa di connessione a un database MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadati
metadata = MetaData()
# la tabella
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')
)
# il mapping
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nome': personnes_table.c.prenom,
'cognome': personnes_table.c.nom,
'età': personnes_table.c.age
})
# la factory di sessione
Session = sessionmaker()
Session.configure(bind=engine)
session = None
try:
# una sessione
session = Session()
# eliminazione della tabella [personnes]
session.execute("drop table if exists personnes")
# ricreazione della tabella a partire dal mapping
metadata.create_all(engine)
# un inserimento
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
# una query
personnes = session.query(Personne).all()
# visualizzazione
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# altri due inserimenti, di cui il secondo fallisce a causa della unicità (nome, cognome)
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}))
# una query
personnes = session.query(Personne).all()
# visualizzazione
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# convalida della sessione
session.commit()
except (InterfaceError, IntegrityError) as erreur:
# visualizzazione
print(f"L'erreur suivante s'est produite : {erreur}")
# annullamento dell'ultima sessione
if session:
print("rollback...")
session.rollback()
finally:
# si liberano le risorse della sessione
if session:
session.close()
Commenti
- righe 1-4: l’applicazione viene configurata;
- righe 7-9: si importano una serie di classi e interfacce dalla libreria [sqlalchemy];
- riga 11: viene importata la classe [Personne];
- riga 14: la stringa di connessione al database. Essa specifica:
- il SGBD utilizzato (mysql);
- il connettore Python utilizzato (mysql.connector senza il punto);
- l'utente che effettua la connessione (admpersonnes);
- la sua password (nobody);
- il computer su cui si trova il SGBD (localhost = computer su cui si trova lo script in esecuzione);
- il nome del database (dbpersonnes);
Con queste informazioni, [sqlalchemy] può connettersi al database. Si noti che il connettore Python utilizzato deve essere già installato. [sqlalchemy] non lo fa.
- righe 19-26: descrizione della tabella [personnes];
- righe 28-34: mappatura tra la classe [Personne] e la tabella [personnes];
- righe 36-38: la maggior parte delle operazioni [sqlalchemy] viene eseguita in una sessione. Il concetto di sessione [sqlalchemy] è simile a quello di transazione SQL. Le sessioni vengono create a partire dalla classe [Session] restituita dalla funzione [sessionmaker] della riga 37;
- riga 38: la classe [Session] è associata al database [dbpersonnes] tramite la stringa di connessione della riga 14;
- riga 43: viene creata una sessione. Come già detto, una sessione può essere paragonata a una transazione;
- righe 45-46: il metodo [Session.execute] consente di eseguire un ordine SQL. Non si tratta di un’operazione comune, poiché è stato detto che il metodo ORM permette di evitare l’uso del linguaggio SQL;
- righe 48-49: il metodo [metadata.create_all] consente di creare tutte le tabelle che utilizzano l’istanza [MetaData] della riga 17. Ne abbiamo solo una: la tabella [personnes] definita alle righe 20-26. [sqlalchemy] utilizzerà le informazioni contenute in queste righe per creare la tabella. Ecco un primo vantaggio di ORM: nasconde le specificità dei SGBD. Infatti, l’ordine SQL [create] può variare notevolmente da un SGBD all’altro a causa dei tipi assegnati alle colonne. Non c’è stata alcuna standardizzazione dei tipi di dati. Pertanto, l’ordine varia da un SGBD all’altro. In questo caso, grazie a [sqlalchemy]:
- descriviamo in modo univoco la tabella che desideriamo;
- [sqlalchemy] riesce a generare il [create] adatto al SGBD che ha di fronte;
- riga 52: si aggiunge un oggetto [Personne] alla sessione. Ciò non lo aggiunge automaticamente al database. Infatti, un ORM segue le proprie regole per sincronizzarsi con il database. Cercherà sempre di ottimizzare il numero di query che esegue. Facciamo un esempio. Lo script aggiunge (add) due persone (persona1, persona2) alla sessione e poi esegue una query: vuole visualizzare tutte le persone presenti nella tabella. [sqlalchemy] può procedere in questo modo:
- l’aggiunta di [personne1] può avvenire in memoria. Per il momento non è necessario inserirlo nel database;
- lo stesso vale per [personne2];
- segue poi la query di tipo [select]. È quindi necessario recuperare tutte le righe della tabella [personnes]. [sqlalchemy] inserirà quindi [personne1, personne2] nel database e poi eseguirà la query;
[sqlalchemy] effettuerà così ottimizzazioni trasparenti per lo sviluppatore.
- riga 56: per eseguire una query di tipo [select] (voglio visualizzare…), si utilizza il metodo [Session.query]. Il parametro del metodo [query] è la classe mappata con la tabella interrogata. Questo metodo restituisce un tipo [Query]. Il metodo [Query.all] richiede tutti gli oggetti [Personne] della sessione. Gli vengono restituite tutte le righe della tabella [personnes], ciascuna sotto forma di un oggetto [Personne]. Per farlo, [sqlalchemy] utilizza la mappatura che è stata effettuata tra la classe [Personne] e la tabella [personnes]. Il risultato della riga 56 è un elenco di oggetti [Personne];
- righe 58-61: vengono visualizzati gli elementi della lista [personnes]. Poiché la classe [Personne] deriva dalla classe [BaseEntity], il metodo [Personne.__str__] utilizzato qui implicitamente nella riga 61 è in realtà il metodo [BaseEntity.__str__] che restituisce la stringa jSON dell'oggetto chiamante. Questa stringa è la stringa jSON del dizionario [Personne.asdict] (cfr. |BaseEntity|). Abbiamo detto che, dopo il mapping, avremmo trovato la proprietà [_sa_instance_state] in ogni oggetto [Personne]. Tuttavia, il valore di questa proprietà non è di tipo [BaseEntity]. È quindi necessario escluderla dal dizionario della classe [Personne], altrimenti la visualizzazione va in "blocco". È ciò che è stato fatto nello script [config];
- righe 63-65: si aggiungono altre due persone che hanno lo stesso nome e cognome. Tuttavia, esiste un vincolo di unicità sull’unione di queste due colonne. Dovrebbe quindi verificarsi un errore. È proprio questo che si sta cercando di verificare;
- righe 67-68: si richiede nuovamente l’elenco di tutte le persone presenti nel database;
- righe 70-73: e le visualizziamo;
- righe 75-76: la sessione viene confermata («commit»). Come suggerisce il nome, la transazione sottostante verrà confermata;
- durante l’esecuzione vedremo che le righe 67-76 non verranno eseguite a causa dell’eccezione generata dalla riga 65. Si passerà quindi alle righe 78-84 per gestire l’eccezione;
- riga 78: l’eccezione [InterfaceError] si verifica se [sqlalchemy] non riesce a connettersi al database [dbpersonnes]. L'eccezione [IntegrityError] si verifica alla riga 65;
- riga 80: viene visualizzato l'errore;
- righe 82-84: se la sessione esiste, viene annullata. Ciò equivale ad annullare la transazione sottostante;
- righe 85-88: in ogni caso, che si verifichi o meno un errore, la sessione viene chiusa per liberare risorse;
I risultati dell’esecuzione sono i seguenti:
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
- righe 2-3: l'elenco delle persone dopo il primo inserimento;
- riga 5: l’eccezione [IntegrityError] verificatasi quando sono state aggiunte due persone con lo stesso nome e cognome;
- righe 6-7: si noti il comando SQL che non è andato a buon fine. Si tratta di un ordine INSERT configurato: [sqlalchemy] ha inserito le due persone con un unico INSERT. Si vede qui che ha cercato di ottimizzare gli ordini SQL emessi;
Ora vediamo, con phpMyAdmin, il contenuto della tabella [personnes]:

In [6] si vede che la tabella è vuota. Non c’è nemmeno la prima persona che lo script aveva inserito nella sessione. Ciò perché la sessione si svolgeva all’interno di una transazione e questa è stata annullata nella clausola [except] dello script [main].
Procediamo ora con la seguente modifica in [main]:
# un inserimento
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
Dopo aver aggiunto una persona alla riga 2, rimuoviamo il commento dalla riga 3. L’operazione [session.commit] convaliderà la transazione sottostante e ne avvierà una nuova. Dopo l’esecuzione, il contenuto della tabella [personnes] è il seguente:

Si nota in [6] che il primo inserimento è stato mantenuto. Ciò è dovuto al fatto che è stato effettuato all’interno della transazione 1, mentre l’errore successivo si è verificato all’interno della transazione 2.
19.3. Script 02: le mappature di [sqlalchemy]

Gli script 02 sono una variante degli script 01. Si cerca di effettuare il maggior numero possibile di configurazioni in [config.py]. Ora si configura l’ambiente [sqlalchemy] dell’applicazione:
def configure():
# percorso assoluto di riferimento dei percorsi relativi della configurazione
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# percorsi assoluti delle dipendenze
absolute_dependencies = [
# BaseEntity, MyException, Persona, Utili
f"{root_dir}/classes/02/entities",
]
# si imposta il syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# importazioni
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper, sessionmaker
# collegamento a un database MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadati
metadata = MetaData()
# la tabella
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')
)
# il mapping
from Personne import Personne
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nome': personnes_table.c.prenom,
'cognome: personnes_table.c.nom,
'età: personnes_table.c.age
})
# la session factory
Session = sessionmaker()
Session.configure(bind=engine)
# si inseriscono queste informazioni nella configurazione
config = {}
config["Session"] = Session
config["metadata"] = metadata
config["engine"] = engine
config["personnes_table"] = personnes_table
# configurazione delle classi
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# si rende la configurazione
return config
Commenti
- righe 2-12: configurazione del Python Path;
- righe 14-45: si configura l'ambiente [sqlalchemy];
- righe 47-52: l’ambiente [sqlalchemy] viene inserito nel dizionario di configurazione;
- righe 54-56: si configura la classe [Personne];
Con questa configurazione, lo script [main] diventa il seguente:
# si configura l'applicazione
import config
config = config.configure()
# il syspath è configurato - si eseguono le importazioni
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError
from Personne import Personne
session = None
try:
# una sessione
session = config["Session"]()
# eliminazione della tabella [personnes]
session.execute("drop table if exists personnes")
# ricreazione della tabella a partire dal mapping
config["metadata"].create_all(config["engine"])
# due inserimenti
session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
session.add(personne)
# convalida dei due inserimenti
session.commit()
# una query
personnes = session.query(Personne).all()
# visualizzazione
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# altri due inserimenti, di cui il secondo non va a buon fine
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
# una richiesta
personnes = session.query(Personne).all()
# visualizzazione
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# convalida della sessione
session.commit()
except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
# visualizzazione
print(f"L'erreur suivante s'est produite : {erreur}")
# annullamento dell'ultima sessione
if session:
print("rollback...")
session.rollback()
finally:
# visualizzazione
print("Travail terminé...")
# si liberano le risorse della sessione
if session:
session.close()
I risultati dell'esecuzione sono i seguenti:
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, la tabella [personnes] è diventata la seguente:

Ora esaminiamo la tabella [personnes] generata da [sqlalchemy]:

- in [6], i tipi utilizzati per le diverse colonne;
- in [7], si vede che la colonna [id] ha l’attributo [AUTO_INCREMENT]. Ciò significa che, durante l’inserimento di una riga nella tabella, se tale riga non ha un valore per la colonna [id], questo verrà generato da MySQL in modo incrementale: 1, 2, 3, … Questa proprietà ci evita di doverci preoccupare del valore della chiave primaria quando effettuiamo un inserimento nella tabella: lasciamo che sia MySQL a generarlo;
- in [8], si nota che la colonna [id] è la chiave primaria;
- in [9], ritroviamo il vincolo di unicità sui campi [nom, prenom];
19.4. Script 03: gestione delle entità della sessione [sqlalchemy]

Il file di configurazione [config] è lo stesso dell’esempio precedente. Nello script [main] si eseguono le operazioni classiche [INSERT, UPDATE, DELETE, SELECT] sulla tabella [personnes] utilizzando i metodi di [sqlalchemy]:
# si configura l'applicazione
import config
config = config.configure()
# importazioni
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne
# visualizza il contenuto della tabella [personnes]
def affiche_table(session: Session):
print("----------------")
# una query
personnes = session.query(Personne).all()
# visualizzazione
affiche_personnes(personnes)
# visualizza un elenco di persone
def affiche_personnes(personnes: list):
print("----------------")
# visualizzazione
for personne in personnes:
print(personne)
# main ---------------------------
session = None
try:
# una sessione
session = config["Session"]()
# eliminazione della tabella [personnes]
# checkfirst=True: verifica prima che la tabella esista
config["personnes_table"].drop(config["engine"], checkfirst=True)
# ricreazione della tabella in base al mapping
config["metadata"].create_all(config["engine"])
# inserimenti
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}))
# viene visualizzato il contenuto della sessione
affiche_table(session)
# elenco delle persone in ordine alfabetico per cognome e, in caso di cognomi uguali, in ordine alfabetico per nome
personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())
# visualizzazione
affiche_personnes(personnes)
# elenco delle persone con un'età compresa nell'intervallo [20,40] in ordine decrescente di età
# poi, a parità di età, in ordine alfabetico per cognome e, a parità di cognome, in ordine alfabetico per nome
personnes = session.query(Personne). \
filter(Personne.âge >= 20, Personne.âge <= 40). \
order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())
# visualizzazione
affiche_personnes(personnes)
# inserimento della sig.ra Bruneau
bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
session.add(bruneau)
# modifica della sua età
bruneau.âge = 47
# elenco delle persone con il cognome Bruneau
personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()
# visualizzazione
affiche_personnes([personne])
# eliminazione della sig.ra Bruneau
session.delete(personne)
# elenco delle persone con il cognome Bruneau
personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")
# visualizzazione
affiche_personnes(personnes)
# conferma della sessione
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# visualizzazione
print(f"L'erreur suivante s'est produite : {erreur}")
# annullamento dell'ultima sessione
if session:
session.rollback()
finally:
# visualizzazione
print("Travail terminé...")
# si liberano le risorse della sessione
if session:
session.close()
Commenti
- righe 20-25: la funzione [affiche_personnes] visualizza gli elementi di un elenco di persone;
- righe 12-18: la funzione [affiche_table] visualizza il contenuto della tabella [personnes];
- righe 34-36: si elimina la tabella [personnes]. A differenza delle versioni precedenti, non si utilizza un comando SQL, ma un metodo di [sqlalchemy]:
- config["personnes_table"] è l'oggetto [Table] che descrive la tabella [personnes];
- config["engine"] è la stringa di connessione al database [dbpersonnes];
- il parametro denominato [checkfirst=True] specifica che l'operazione venga eseguita solo se la tabella [personnes] esiste;
- righe 38-39: la tabella [personnes] viene ricreata;
- righe 41-44: tre persone vengono inserite nella sessione. Si ricorda che non vengono necessariamente inserite immediatamente nella tabella [personnes]. Ciò dipende dalla strategia di [sqlalchemy], volta a ottimizzare le prestazioni;
- righe 46-47: viene visualizzato il contenuto della tabella [personnes]. Se gli inserimenti delle tre persone non erano ancora stati effettuati, vengono ora eseguiti a seguito di questa richiesta;
- righe 49-50: un esempio di utilizzo del metodo [order_by] che consente di presentare i risultati di una query in un determinato ordine. La sintassi [order_by(critère1, critère2)] visualizza i risultati innanzitutto in base al criterio [critère1] e, quando le righe presentano lo stesso valore di [critère1], vengono ordinate in base al criterio [critère2]. È possibile specificare più criteri in questo modo;
- righe 55-59: introducono il concetto di filtro con il metodo [filter]. La notazione [filter(critère1, critère2)] crea un ET logico (AND) tra i criteri utilizzati;
- righe 64-67: viene avviata una nuova sessione per un utente;
- righe 70-71: un altro esempio di query filtrata. La funzione [func.lower(param)] converte [param] in minuscolo. Sono disponibili anche altre funzioni, contrassegnate con [func.xx]. Nell'espressione della riga 71:
- [session.query.filter] restituisce un elenco di oggetti [Personne];
- [session.query.filter.first] restituisce il primo elemento di tale elenco;
- riga 77: si elimina un elemento dalla sessione;
- riga 86: la sessione viene convalidata;
I risultati dell'esecuzione sono i seguenti:
- righe 4-6: il contenuto della sessione;
- righe 8-10: il contenuto della sessione in ordine decrescente dei nomi;
- righe 12-13: il contenuto della sessione per le persone la cui età rientra nell'intervallo [20, 40];
- riga 15: la persona di nome «bruneau»;
In phpMyAdmin, il contenuto della tabella [personnes] al termine dell’esecuzione è il seguente:

19.5. Script 04: utilizzo di un database [PostgreSQL]

La cartella [04] è una copia della cartella [03]. Si modifica un solo elemento, la stringa di connessione nel file [config]:
# collegamento a un database PostgreSQL
engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")
Ora questa stringa di connessione fa riferimento al database [dbpersonnes] di un SGBD [PostgreSQL]. Si noti l’utilizzo del connettore [psycopg2]. È necessario che quest’ultimo sia installato.
L'esecuzione dello script [main] fornisce i seguenti risultati:
Con lo strumento [pgAdmin] (cfr. paragrafo |pgAdmin|), la tabella [personnes] presenta il seguente stato:

La tabella [personnes] è stata generata con il codice SQL seguente:

- in [4-5], si nota che la colonna [id] è la chiave primaria. Si nota inoltre che essa ha un valore predefinito [mot clé DEFAULT], il che fa sì che, se si inserisce una riga senza chiave primaria, questa venga generata dal SGBD. Si tratta di un funzionamento comune: si lascia che sia il SGBD a generare le chiavi primarie;
Questa versione 05 degli script [sqlalchemy] dimostra chiaramente quanto sia facile passare da un SGBD all’altro: è bastato modificare la stringa di connessione in uno script di configurazione. Nient’altro è cambiato. Se si confrontano i tipi delle colonne di [id, nom, prenom, age] sopra riportate con quelli della tabella MySQL dell’esempio |02|, si nota che sono diversi. [sqlalchemy] li adatta al SGBD utilizzato. Questa facilità di adattamento a un nuovo SGBD è un motivo sufficiente per adottare [sqlalchemy] o un altro ORM.
19.6. Script 05: esempio completo

L’esempio esaminato riprende quello analizzato nel paragrafo |troiscouches-v01|. Tale esempio presentava un’architettura a tre livelli [ui, métier, dao] che gestiva entità [Classe, Elève, Matière, Note]. Le entità erano codificate in modo statico in un livello [dao]. Ora le inseriamo in un database. Utilizzeremo due SGBD: MySQL e PostgreSQL.
19.6.1. L’architettura dell’applicazione
L’architettura dell’applicazione sarà la seguente:

- In [1-3] si trovano i livelli [ui, métier, dao] già presenti nell'esempio |troiscouches-v01|. Il livello [dao] comunica ora con il livello [ORM];
- gli strati [1-5] sono implementati tramite codice Python;
19.6.2. I database
Creiamo un database MySQL denominato [dbecole] di proprietà dell’utente [admecole] con password [mdpecole]. A tal fine seguiamo la procedura descritta nel paragrafo |creazione di un database|:


- in [1], il database [dbecole] senza le tabelle [3];
- nel database [7], l'utente [admecole] dispone di tutti i privilegi su questo database;
Si procede allo stesso modo con SGBD e PostgreSQL. Creiamo un database denominato [dbecole] di proprietà dell’utente [admecole] con password [mdpecole]. A tal fine seguiamo la procedura descritta nel paragrafo |creazione di un database|:

- in [1], il database [dbecole];
- in [2], l’utente [admecole];
- in [3-4], il database [dbecole] è di proprietà dell’utente [admecole];
19.6.3. Le entità gestite dall’applicazione
Nell’applicazione |troiscouches v01|, le entità gestite erano le seguenti (cfr. |entità|). Sono proprio queste entità che verranno memorizzate nei database precedenti. Non duplicheremo queste entità nella nuova applicazione. Le recupereremo da dove sono già definite.
La classe [Classe]:
# importazioni
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# attributi esclusi dallo stato della classe
excluded_keys = []
# proprietà della classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificatore della classe
# nome: nome della classe
return BaseEntity.get_allowed_keys() + ["nom"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
# setter
@nom.setter
def nom(self: object, nom: str):
# nome: deve essere una stringa non vuota
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")
La classe [Elève]:
# importazioni
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# attributi esclusi dallo stato della classe
excluded_keys = []
# proprietà della classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificativo dello studente
# cognome: cognome dello studente
# nome: nome dello studente
# classe: classe dello studente
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
# setter
@nom.setter
def nom(self: object, nom: str) -> str:
# il cognome deve essere una stringa non vuota
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:
# il nome deve essere una stringa non vuota
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:
# si richiede un tipo Classe
if isinstance(value, Classe):
self.__classe = value
# oppure un tipo "dict"
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# oppure un tipo 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}")
La classe [Matière]:
# importazioni
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# attributi esclusi dallo stato della classe
excluded_keys = []
# proprietà della classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificativo della materia
# nome: nome della materia
# coefficiente: coefficiente della materia
return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
@property
def coefficient(self: object) -> float:
return self.__coefficient
# setter
@nom.setter
def nom(self: object, nom: str):
# il nome deve essere una stringa non vuota
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):
# il coefficiente deve essere un numero reale >=0
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# errore?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
La classe [Note]:
# importazioni
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# attributi esclusi dallo stato della classe
excluded_keys = []
# proprietà della classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificativo della nota
# valore: il voto stesso
# studente: studente (di tipo Studente) a cui si riferisce il voto
# materia: materia (di tipo Materia) a cui si riferisce il voto
# l’oggetto «Voto» rappresenta quindi il voto di uno studente in una materia
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# getter
@property
def valeur(self: object) -> float:
return self.__valeur
@property
def élève(self: object) -> Elève:
return self.__élève
@property
def matière(self: object) -> Matière:
return self.__matière
# getter
@valeur.setter
def valeur(self: object, valeur: float):
# il voto deve essere un numero reale compreso tra 0 e 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:
# si richiede un tipo «Studente»
if isinstance(value, Elève):
self.__élève = value
# oppure un tipo dict
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# oppure un tipo 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:
# si richiede un tipo "Materia"
if isinstance(value, Matière):
self.__matière = value
# o un tipo dict
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# o un tipo 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. Configurazione

La configurazione è stata suddivisa in diversi file:
- la configurazione generale in [config.py]: definisce il Python Path dell’applicazione e istanzia i livelli dell’architettura;
- la configurazione di [sqlalchemy] in [config_database]: effettua le mappature tra classi e tabelle;
- i livelli dell’applicazione sono configurati in [config_layers];
Il file [config] è il seguente:
def configure(config: dict) -> dict:
import os
# fase 1 ---
# si imposta il Python Path dell'applicazione
# percorso assoluto della cartella contenente questo script
script_dir = os.path.dirname(os.path.abspath(__file__))
# percorso assoluto di riferimento per i percorsi relativi della configurazione
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# percorsi assoluti delle dipendenze
absolute_dependencies = [
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# progetto a tre livelli v01
f"{root_dir}/troiscouches/v01/interfaces",
f"{root_dir}/troiscouches/v01/services",
f"{root_dir}/troiscouches/v01/entities",
# documenti del presente progetto
script_dir,
f"{script_dir}/../services",
]
# aggiornamento del syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# fase 2 ------
# configurazione del database
import config_database
config = config_database.configure(config)
# fase 3 ------
# istanziazione dei livelli dell'applicazione
import config_layers
config = config_layers.configure(config)
# si esegue la configurazione
return config
- righe 4-27: creazione del Python Path dell’applicazione;
- righe 29-32: configurazione di [sqlalchemy];
- righe 34-37: configurazione dei livelli dell'applicazione;
Il file [config_database] è il seguente:
def configure(config: dict) -> dict:
# config['sgbd'] è il nome del SGBD utilizzato
# MySQL: MySQL
# pgres: PostgreSQL
# configurazione SQLAlchemy
from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine
from sqlalchemy.orm import mapper, relationship, sessionmaker
# stringhe di connessione ai database utilizzati
engines = {
'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
}
# stringa di connessione al database in uso
engine = create_engine(engines[config['sgbd']])
# metadati
metadata = MetaData()
# le tabelle del database
tables = {}
# le classi mappate
from Classe import Classe
from Elève import Elève
from Note import Note
from Matière import Matière
# la tabella delle classi
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,
'nome': classes_table.c.nom
})
# la tabella degli studenti
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),
# uno studente appartiene a una classe
Column('classe_id', Integer, ForeignKey('classes.id')),
)
# mappatura
mapper(Elève, tables['élèves'], properties={
'id': élèves_table.c.id,
'cognome': élèves_table.c.nom,
'nome': élèves_table.c.prénom,
'classe': relationship(Classe, backref="studenti", lazy="select")
})
# l'indice
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)
)
# mappatura
mapper(Matière, tables['matières'], properties={
'id': matières_table.c.id,
'nome': matières_table.c.nom,
"coefficient": matières_table.c.coefficient
})
# la tabella dei voti
tables['notes'] = notes_table = \
Table("notes", metadata,
Column('id', Integer, primary_key=True),
Column('valeur', Float, nullable=False),
# un voto è quello di uno studente
Column('élève_id', Integer, ForeignKey('élèves.id')),
# un voto è quello di una materia
Column('matière_id', Integer, ForeignKey('matières.id')),
)
# mappatura
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'valore': notes_table.c.valeur,
'studente': relazione(Studente, backref="note", lazy="select"),
'materia': relazione(Materia, backref="note", lazy="select")
})
# configurazione delle entità [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']
# la session factory
Session = sessionmaker()
Session.configure(bind=engine)
# una sessione
session = Session()
# si salvano alcune informazioni nel dizionario di configurazione
config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
# si rende la configurazione
return config
Commenti
- righe 1-4: la funzione [configure] riceve un dizionario come parametro. Viene utilizzata solo la chiave [sgbd]. Il suo valore è [mysql] se il database è un database MySQL, [pgres] se il database è un database PostgreSQL;
- righe 6-9: importazione di elementi da [sqlalchemy]. Lo script [config_database] effettua le mappature tra le tabelle del database [dbecole] e le entità [Classes, Elève, Matière, Note]. Nella tabella, i dati dell’entità sono incapsulati in una riga. Nel codice Python, sono racchiusi in un oggetto. Da qui il nome ORM (Object Relational Mapper): ORM effettua una mappatura (un collegamento) tra le righe di un database relazionale e gli oggetti. In questa applicazione, abbiamo quattro entità [Classe, Elève, Matière, Note] che saranno collegate a quattro tabelle [classes, élèves, matières, notes]. Si noti che i nomi delle tabelle possono contenere caratteri accentati;
- righe 11-17: la stringa di connessione al database utilizzato. Questa dipende dall’elemento config[‘sgbd’];
- righe 24-28: le entità dell’applicazione che saranno oggetto di un mapping [sqlalchemy]. Quando queste righe verranno eseguite, il Python Path sarà già stato impostato dallo script [config];
- righe 30-40: il mapping tra l’entità [Classe] e la tabella [classes];
- righe 30-35: la tabella [classes] viene definita con la classe [Table] di [sqlalchemy]. Si specifica che questa tabella ha due colonne:
- la colonna [id], che è la chiave primaria e corrisponde al numero della classe, riga 33;
- la colonna [nom], che contiene il nome della classe, riga 34;
- righe 31-32: si noti che la sintassi x=y=z è valida in Python: il valore di z viene assegnato a y, poi il valore di y a x;
- righe 37-40: si elencano le corrispondenze tra le colonne della tabella [classes] e le proprietà dell’entità [Classe];
- righe 42-57: la mappatura tra l’entità [Elève] e la tabella [élèves];
- righe 51-57: la tabella [élèves] è definita con la classe [Table] di [sqlalchemy]. Si specifica che questa tabella ha quattro colonne:
- la colonna [id], che è la chiave primaria e corrisponde al numero dello studente, riga 45;
- la colonna [nom], che contiene il cognome dello studente, riga 46;
- la colonna [prénom], che contiene il nome dello studente, riga 47. Si noti che il nome di una colonna può contenere caratteri accentati;
- riga 49, la colonna [classe_id], che conterrà il numero della classe a cui appartiene lo studente. Si parla in questo caso di chiave esterna. [élèves.classe_id] è una chiave esterna (ForeignKey) sulla colonna [classes.id]. Ciò significa che il valore di [élèves.classe_id] deve essere presente nella colonna [classes.id];
- righe 51-57: vengono elencate le corrispondenze tra le colonne della tabella [élèves] e le proprietà dell’entità [Elève]:
- le righe 53-55 sono facili da comprendere;
- la riga 56 è più complessa: definisce il valore della proprietà [Elève.classe] come calcolato dalla relazione (relationship) di chiave esterna che collega le tabelle [élèves] e [classes]. I parametri della funzione [relationship] sono i seguenti:
- [Classe]: è il nome dell'entità con cui l'entità [Elève] ha una relazione di chiave esterna. Tale relazione deve concretizzarsi nella tabella [élèves] tramite la presenza di una chiave esterna nella tabella [classes]. Sappiamo che questa esiste;
- [backref="élèves"]: il nome di una proprietà che verrà aggiunta all’entità [Classe]. [Classe.élèves] sarà l’elenco di tutti gli studenti della classe. Questa proprietà non deve già esistere. Se esiste già, basta semplicemente scegliere qui un altro nome per [backref]. Lo sviluppatore non deve gestire questa proprietà. Ci penserà [sqlalchemy]. Deve semplicemente sapere che esiste, che è stata aggiunta da [sqlalchemy] e che può utilizzarla nel proprio codice;
- [lazy=’select’]: ciò significa che ORM non deve cercare di assegnare immediatamente un valore alla proprietà [Elève.classe]. Deve cercare il suo valore solo quando il codice lo richiede esplicitamente. Quindi:
- se il codice richiede l’elenco di tutti gli studenti, questi verranno restituiti ma la loro proprietà [classe] non verrà calcolata;
- poco dopo, il codice si interessa a uno studente specifico [e] e fa riferimento alla sua classe [e.classe]. Questo riferimento costringerà quindi [sqlalchemy] a eseguire una query sul database per recuperare la classe dello studente, in modo trasparente per lo sviluppatore;
- inoltre, l’inserimento di [lazy=’select’] mira a evitare richieste inutili al database;
- riga 56: quando ORM recupera una riga dalla tabella [élèves], recupera le informazioni [id, nom, prénom, classe_id]. A partire da lì, deve costruire un oggetto Studente(id, cognome, nome, classe). Per le proprietà [id, nom, prénom] non ci sono difficoltà. Per la proprietà [classe], invece, la questione è più complessa. Il suo valore è un riferimento a un oggetto di tipo [Classe]. Tuttavia, l’oggetto ORM dispone solo di un’informazione [élèves.classe_id]. Poiché [élèves.classe_id] è una chiave esterna sulla colonna [classes.id], qui gli viene indicato di utilizzare questa relazione per recuperare dalla tabella [classes] la riga con id=[élèves.classe_id] (che esiste necessariamente) e di creare, a partire da tale riga, l’oggetto [Classe] richiesto dalla proprietà [Elève.classe];
- righe 59-71: la mappatura tra l’entità [Matière] e la tabella [matières];
- righe 59-65: definizione della tabella [sqlalchemy] denominata [matières];
- righe 66-71: vengono elencate le corrispondenze tra le colonne della tabella [matières] e le proprietà dell’entità [Matière]. Qui non ci sono difficoltà;
- righe 73-90: il mapping tra l’entità [Note] e la tabella [notes];
- righe 73-82: definizione della tabella [sqlalchemy] denominata [notes]. Presenta due chiavi esterne:
- riga 79, la colonna [notes.élève_id] prende i propri valori dalla colonna [élèves.id]]. Questa chiave esterna indica che una nota è quella di uno studente specifico;
- riga 81, la colonna [notes.matière_id] prende i propri valori dalla colonna [matières.id]. Questa chiave esterna indica che un voto è un voto in una materia specifica;
- righe 84-90: la mappatura tra l’entità [Note] e la tabella [notes]:
- riga 88: la proprietà [Note.élève] deve avere come valore un'istanza di tipo [Elève]. L'ORM contiene, nella riga della tabella [notes], solo la colonna [notes.élève_id] che fa riferimento alla colonna [élèves.id]. In questo caso si indica di utilizzare questa relazione di chiave esterna per recuperare l’istanza [Elève] di cui si conosce il voto. Inoltre, [relationship(Elève, backref="notes", …)] creerà la nuova proprietà [Elève.notes], che conterrà l’elenco dei voti dello studente. Questa proprietà non deve già esistere nella classe [Elève];
- riga 89: la proprietà [Note.matière] deve avere come valore un’istanza di tipo [Matière]. L’istanza ORM contiene, nella riga della tabella [notes], solo la colonna [notes.matière_id] che fa riferimento alla colonna [matières.id]. In questo caso si indica di utilizzare questa relazione di chiave esterna per recuperare l’istanza [Matière] di cui si conosce il voto. Inoltre, [relationship(Matière, backref="notes", …)] creerà la nuova proprietà [Matière.notes], che costituirà l’elenco delle note relative alla materia. Questa proprietà non deve già esistere nella classe [Matière];
- righe 92-96: si definisce, per ogni entità derivata da [BaseEntity], l’elenco delle proprietà da escludere dal dizionario delle proprietà dell’entità (BaseEntity.asdict). Abbiamo visto che [sqlalchemy] aggiungeva la proprietà [_sa_instance_state] a tutte le entità mappate. Non la vogliamo nel dizionario delle proprietà. Inoltre, abbiamo visto che le mappature precedenti avevano aggiunto nuove proprietà alle entità:
- [Elève.notes]: tutti i voti dello studente;
- [Classe.élèves]: tutti gli studenti della classe;
- [Matière.notes]: tutti i voti della materia;
In generale, non si desidera che queste proprietà vengano aggiunte allo stato dell’entità. Infatti, calcolarne il valore ha un costo SQL e tale valore è spesso inutile. Pertanto, se si recupera lo studente di nome ‘X’:
- (continua)
- ORM restituirà un’entità [Elève(id, nom, prénom, classe, notes)]. A causa di [lazy=’select’], le proprietà [classe, notes] legate alle chiavi esterne del database non saranno state calcolate;
- ora, se visualizzo la stringa jSON di questo studente, sappiamo che sarà la stringa jSON del dizionario [asdict] dell’entità. Se le proprietà [classe] e [notes] sono presenti, [sqlalchemy] sarà costretta a eseguire query su SQL per calcolarne i valori. Si tratta di un'operazione costosa. Se è possibile evitare queste query, è preferibile;
- in questo caso, abbiamo escluso tutte le proprietà legate a una chiave esterna;
- righe 98-100: istanziazione e configurazione di un [Session factory] (factory=fabbrica di produzione). L’oggetto [Session] serve a creare sessioni [sqlalchemy] associate a transazioni;
- righe 102-103: creazione di una sessione sqlalchemy];
- riga 106: alcuni elementi della configurazione [sqlalchemy] vengono inseriti nel dizionario globale della configurazione dell’applicazione;
- riga 109: si restituisce questo dizionario;
Il file [config_layers] configura i livelli dell’applicazione:
def configure(config: dict) -> dict:
# istanziazione del livello [dao]
from DatabaseDao import DatabaseDao
dao = DatabaseDao(config)
# istanziazione del livello [métier]
from Métier import Métier
métier = Métier(dao)
# istanza del livello [ui]
from Console import Console
ui = Console(métier)
# si inseriscono i livelli nella configurazione
config['dao'] = dao
config['métier'] = métier
config['ui'] = ui
# si genera la configurazione
return config
- riga 1: la funzione [configure] riceve il dizionario della configurazione globale dell'applicazione;
- righe 2-12: vengono istanziate le classi dell’applicazione;
- righe 15-17: i riferimenti dei livelli vengono inseriti nella configurazione globale;
- riga 20: viene restituita la nuova configurazione;
19.6.5. Il livello [dao] - 1

È importante comprendere che il livello [dao] [3] comunica con l’ORM [sqlalchemy] [4] configurato come descritto nel paragrafo precedente. Dei tre livelli [ui, métier, dao] dell’applicazione |troiscouches v01|, solo il livello [dao] deve essere riscritto. I livelli [ui, métier] vengono mantenuti.
L’implementazione del livello [dao] è stata inserita nella cartella [services]:

[InterfaceDatabaseDao] è l’interfaccia del livello [dao]:
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
class InterfaceDatabaseDao(InterfaceDao, ABC):
# inizializzazione del database
@abstractmethod
def init_database(self, data: dict):
pass
- riga 6: l’interfaccia [InterfaceDatabaseDao] deriva sia dalla classe [ABC], in quanto classe astratta, sia dall’interfaccia [InterfaceDao] del progetto |troiscouches v01|;
- righe 8-11: si aggiunge il metodo [init_database] ai metodi ereditati da [InterfaceDao]. Il suo ruolo sarà quello di inizializzare il database con i dati del dizionario [data] che gli vengono passati come parametro alla riga 10;
Ricordiamo che l’interfaccia [InterfaceDao] era la seguente:
# importazioni
from abc import ABC, abstractmethod
# interfaccia DAO
from Elève import Elève
class InterfaceDao(ABC):
# elenco delle classi
@abstractmethod
def get_classes(self: object) -> list:
pass
# elenco degli studenti
@abstractmethod
def get_élèves(self: object) -> list:
pass
# elenco delle materie
@abstractmethod
def get_matières(self: object) -> list:
pass
# elenco dei voti
@abstractmethod
def get_notes(self: object) -> list:
pass
# elenco dei voti di uno studente
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# cerca uno studente in base al suo ID
@abstractmethod
def get_élève_by_id(self: object, élève_id: int) -> Elève:
pass
L’implementazione del livello [dao] è la seguente:
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):
…
…
- riga 11: la classe [DatabaseDao] implementa l’interfaccia [InterfaceDatabaseDao];
- righe 13-16: il costruttore della classe. Riceve come parametro il dizionario della configurazione dell’applicazione;
- riga 15: si memorizza la configurazione [sqlalchemy];
- riga 16: si memorizza la sessione [sqlalchemy] attraverso la quale si opererà sul database;
- riga 18: il metodo [init_database] inizializza il database con il dizionario [data];
Il dizionario [data] è implementato dal seguente 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
# si istanziano le classi
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
classes = [classe1, classe2]
# le materie
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]
# gli studenti
é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]
# i voti degli studenti nelle diverse materie
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]
# si raggruppa l'insieme
data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
# si rendono disponibili i dati
return data
- riga 34: il dizionario che verrà passato al metodo [init_database]. Questo dizionario è composto dalle seguenti chiavi (riga 32):
- [élèves]: l'elenco degli studenti;
- [classes]: l'elenco delle classi;
- [matières]: l'elenco delle materie;
- [notes]: l'elenco dei voti di tutti gli studenti in tutte le materie;
Torniamo al metodo [init_database]:
def init_database(self, data: dict):
# configurazione del database
database = self.database
engine = database['engine']
metadata = database['metadata']
tables = database['tables']
try:
# eliminazione delle tabelle esistenti
# checkfirst=True: verifica prima che la tabella esista
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)
# ricreazione delle tabelle in base al mapping
metadata.create_all(engine)
# compilazione delle tabelle
session = self.session
# classi
classes = data["classes"]
for classe in classes:
session.add(classe)
# materie
matières = data["matières"]
for matière in matières:
session.add(matière)
# studenti
élèves = data["élèves"]
for élève in élèves:
session.add(élève)
# voti
notes = data["notes"]
for note in notes:
session.add(note)
# conferma
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# annullamento della sessione
if session:
session.rollback()
# si genera l'eccezione
raise MyException(23, f"{erreur}")
- righe 3-6: si recuperano informazioni dalla configurazione del database;
- righe 9-14: abbiamo visto che la configurazione [sqlalchemy] aveva mappato quattro entità su quattro tabelle [élèves, matières, classes, notes]. Si inizia eliminando queste tabelle, se esistono;
- righe 16-17: si ricreano le quattro tabelle appena eliminate;
- righe 22-25: inseriamo tutte le classi nella sessione;
- righe 27-30: si inseriscono tutte le materie nella sessione;
- righe 32-35: si inseriscono tutti gli studenti nella sessione;
- righe 37-40: si inseriscono tutti i voti nella sessione;
- per effettuare queste aggiunte, abbiamo seguito un ordine preciso. Abbiamo iniziato dalle entità che non avevano relazioni con altre entità per finire con quelle che ne avevano. In questo modo, quando si aggiungono gli studenti alla sessione, le classi a cui questi fanno riferimento sono già presenti nella sessione;
- riga 43: la sessione [sqlalchemy] è stata convalidata. Dopo questa operazione, si ha la certezza che tutti i dati della sessione siano stati sincronizzati con il database. In altre parole, sono stati inseriti nelle tabelle. Ciò è stato possibile grazie alle mappature definite nella configurazione di [sqlalchemy]. [sqlalchemy] sa come ogni entità debba essere memorizzata nelle tabelle. [sqlalchemy] ha inoltre generato le chiavi esterne che le tabelle possono contenere;
- righe 44-49: se si verifica un problema, la sessione [sqlalchemy] viene annullata e, alla riga 49, viene generata un'eccezione;
19.6.6. Inizializzazione del database

Lo script [main_init_database] inizializza il database con il contenuto dello script [data.py]. Il suo codice è il seguente:
# si attende un parametro mysql o 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()
# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})
# il syspath è configurato - è possibile eseguire le importazioni
from MyException import MyException
# si recuperano i dati da inserire nel database
import data
data = data.configure()
# si recupera il livello [dao]
dao = config["dao"]
# ----------- main
try:
# creazione e inizializzazione delle tabelle del database
dao.init_database(data)
except MyException as ex:
# viene visualizzato l'errore
print(f"L'erreur suivante s'est produite : {ex}")
finally:
# liberazione delle risorse utilizzate dall'applicazione
import shutdown
shutdown.execute(config)
# fine
print("Travail terminé...")
- righe 1-11: lo script attende un parametro [mysql] o [pgres] a seconda che si voglia inizializzare un database MySQL o PostgreSQL;
- righe 13-15: l'applicazione è configurata per il parametro SGBD passato in parametro;
- righe 20-22: si recuperano i dati da inserire nel database;
- riga 25: il livello [dao] è già stato istanziato ed è accessibile nella configurazione dell’applicazione;
- riga 30: il database viene inizializzato;
- righe 34-37: indipendentemente dalla presenza o meno di errori, si liberano le risorse dell’applicazione tramite il modulo [shutdown];
Il modulo [shutdown.py] è il seguente:
def execute(config: dict):
# si liberano le risorse utilizzate dall'applicazione
sqlalchemy_session = config['database']['session']
if sqlalchemy_session:
sqlalchemy_session.close()
La funzione [shutdown.execute] chiude la sessione [sqlalchemy] utilizzata per inizializzare il database.
Creiamo una prima configurazione di esecuzione (cfr. |configurazione di esecuzione|) per eseguire [main_init_database] con SGBD e MySQL:

I risultati dell’esecuzione di questa configurazione sono i seguenti in phpMyAdmin:



Per SGBD e [PostgreSQL], utilizziamo la seguente configurazione di esecuzione:

All’esecuzione, i risultati in [pgAdmin] sono i seguenti:



Si noti la facilità con cui è stato possibile passare a SGBD.
19.6.7. Il livello [dao] – 2
Torniamo alla classe [DatabaseDao] che implementa il livello [dao]. Finora abbiamo mostrato solo l’implementazione del metodo [init_database]. Ora mostriamo l’implementazione degli altri metodi:
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):
…
# elenco di tutte le classi
def get_classes(self: object) -> list:
# richiesta
return self.session.query(Classe).all()
# elenco di tutti gli studenti
def get_élèves(self: object) -> list:
# query
return self.session.query(Elève).all()
# elenco di tutte le materie
def get_matières(self: object) -> list:
# richiesta
return self.session.query(Matière).all()
# l'elenco dei voti di tutti gli studenti
def get_notes(self: object) -> list:
# richiesta
return self.session.query(Note).all()
# elenco dei voti di uno studente specifico
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
# si cerca l'alunno - se non esiste, viene generata un'eccezione
# si lascia che venga visualizzata
élève = self.get_élève_by_id(élève_id)
# si recuperano i suoi voti (lazy loading)
notes = élève.notes
# si restituisce un dizionario
return {"élève": élève, "notes": notes}
# uno studente individuato tramite il suo numero
def get_élève_by_id(self, élève_id: int) -> Elève:
# si cerca lo studente
élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
# L'abbiamo trovato?
if élèves:
return élèves[0]
else:
raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")
# uno studente identificato per nome
def get_élève_by_name(self, élève_name: str) -> Elève:
# si sta cercando lo studente
élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
# L'abbiamo trovato?
if élèves:
return élèves[0]
else:
raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")
# una classe individuata in base al numero
def get_classe_by_id(self, classe_id: int) -> Classe:
# si sta cercando la classe
classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
# L'abbiamo trovata?
if classes:
return classes[0]
else:
raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")
# una classe identificata dal suo nome
def get_classe_by_name(self, classe_name: str) -> Classe:
# si sta cercando la classe
classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
# l'abbiamo trovata?
if classes:
return classes[0]
else:
raise MyException(14, f"La classe de nom {classe_name} n'existe pas")
# una materia identificata dal suo numero
def get_matière_by_id(self, matière_id: int) -> Matière:
# si sta cercando la materia
matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
# L'abbiamo trovata?
if matières:
return matières[0]
else:
raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")
# un materiale identificato dal suo nome
def get_matière_by_name(self, matière_name: str) -> Matière:
# si sta cercando la materia
matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
# L'abbiamo trovato?
if matières:
return matières[0]
else:
raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
- righe 21-24: il metodo [get_classes] deve restituire l'elenco delle classi della scuola. Alla riga 20 utilizziamo una query già vista;
- righe 26-39: altri tre metodi simili per ottenere gli elenchi degli studenti, delle materie e dei voti;
- righe 51-59: il metodo [get_élève_by_id] deve restituire uno studente identificato dal suo numero. Genera un'eccezione se quest'ultimo non esiste;
- riga 54: si utilizza una query filtrata. Si ottiene un elenco vuoto o contenente un elemento;
- riga 57: se l'elenco recuperato non è vuoto, viene restituito il primo elemento dell'elenco;
- altrimenti, alla riga 59, viene generata un'eccezione;
- righe 41-49: il metodo [get_notes_for_élève_by_id] deve restituire i voti di uno studente identificato dal suo numero:
- riga 45: si utilizza il metodo [get_élève_by_id] per ottenere l’entità Studente dello studente;
- riga 47: si utilizza la proprietà [Elève.notes] creata dal mapping tra l’entità [Note] e la tabella [notes] (cfr. paragrafo |configurazione SQLAlchemy|) e che rappresenta i voti dello studente;
- riga 49: si restituisce un dizionario;
- righe 61-109: una serie di metodi analoghi che consentono di:
- trovare uno studente in base al nome, righe 61-69;
- ricercare una classe, righe 71-89;
- ricercare una materia, righe 91-109;
19.6.8. Lo script [main_joined_queries]

Lo script [main_joined_queries] si chiama così perché ha lo scopo di mettere in evidenza le query eseguite implicitamente da [sqlalchemy] per recuperare informazioni appartenenti a più tabelle. Queste query, nascoste al programmatore, vengono eseguite ogni volta che una proprietà di un’entità è stata associata alla funzione [relationship] nel mapping dell’entità. Ad esempio:
# mappatura
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'valore': notes_table.c.valeur,
'studente': relazione(Studente, backref="note", lazy="select"),
'materia': relazione(Materia, backref="note", lazy="select")
})
Sopra, il mapping tra l’entità [Note] e la tabella [notes]:
- riga 5: quando la proprietà [élève] di un’entità [Note] viene richiesta per la prima volta, verrà ricercata nella tabella [élèves] tramite una query SQL. Finché questa proprietà non viene richiesta, rimane indefinita (lazy load). Una volta ottenuta, il suo valore rimane nella memoria di ORM. Quando verrà richiamata una seconda volta, l’ORM fornirà immediatamente il suo valore senza ricorrere a una nuova richiesta SQL. Tutto ciò è trasparente per lo sviluppatore;
- lo stesso vale per la proprietà inversa [Elève.notes] (backref), riga 5;
- lo stesso vale per la proprietà [Note.matière] e la sua proprietà inversa [Matière.notes] (backref), riga 6;
Lo script [main_joined_queries] è il seguente:
# si attende un parametro mysql o 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()
# si configura l'applicazione
import config
config = config.configure({"sgbd": sgbd})
# il syspath è configurato - è possibile eseguire le importazioni
from MyException import MyException
# il livello [dao]
dao = config["dao"]
try:
# studente per ID
print("élève id=11 -----------")
élève = dao.get_élève_by_id(11)
print(f"élève={élève}")
# la classe dello studente (lazy loading)
classe = élève.classe
print(f"classe de l'élève : {classe}")
# gli studenti della stessa classe (caricamento differito)
print("élèves dans la même classe :")
for élève in classe.élèves:
print(f"élève={élève}")
# uno studente in base al nome
print("élève nom='nom2' -----------")
print(f"élève={dao.get_élève_by_name('nom2')}")
# la sua classe (caricamento differito)
print(f"classe de l'élève : {élève.classe}")
# voti di uno studente
print("notes de l'élève id=11 -----------")
# prima l'alunno
élève = dao.get_élève_by_id(11)
# poi i suoi voti (lazy loading)
for note in élève.notes:
# il voto
print(f"note={note}, "
# la materia del voto (caricamento differito)
f"matière={note.matière}")
# gli studenti di una classe
print("élèves de la classe nom='classe1' -----------")
# prima la classe
classe = dao.get_classe_by_name('classe1')
# poi gli studenti (caricamento differito)
for élève in classe.élèves:
print(élève)
# lo stesso vale per [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)
# i voti in una materia
print("matière de nom='matière1' -----------")
# prima la materia
matière = dao.get_matière_by_name('matière1')
print(f"matière={matière}")
# poi i voti in quella materia (caricamento differito)
print("Notes dans la matière : ")
for note in matière.notes:
print(note)
# lo stesso vale per materia2
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:
# viene visualizzato l'errore
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# viene visualizzato l'errore
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# si liberano le risorse
import shutdown
shutdown.execute(config)
I commenti sono sufficienti per comprendere il codice.
Si crea una configurazione di esecuzione per MySQL:

I risultati dell'esecuzione sono i seguenti:
Per comprendere questi risultati, occorre ricordare che sono state escluse alcune proprietà dal dizionario delle entità (cfr. |configurazione|):
# configurazione delle entità [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']
Pertanto, quando si digita [print(f"élève={élève}")] alla riga 26 del codice, la riga 1 sopra riportata indica che le proprietà ['_sa_instance_state', 'notes', 'classe'] non verranno visualizzate. È ciò che si osserva alla riga 3 dei risultati. Tutte le altre proprietà vengono visualizzate. Pertanto, sempre alla riga 3, si scopre una nuova proprietà [classe_id] che inizialmente non esisteva nell’entità [Elève]. Questa proprietà corrisponde direttamente alla colonna [classe_id] della tabella [élèves]. Pertanto, [sqlalchemy] ha aggiunto le seguenti proprietà all’entità [Elève]: [classe_id, _sa_instance_state, notes]. È importante esserne consapevoli, soprattutto perché tali proprietà non devono già esistere nell’entità mappata.
Le proprietà escluse dal dizionario delle entità sono importanti. Se, ad esempio, non si escludono le proprietà [notes, élève] dall’entità [Elève], l’operazione [print(f"élève={élève}")] le visualizzerà e, come appena spiegato, genererà delle richieste implicite SQL (lazy loading) per recuperare i valori di tali proprietà. Se, come in questo caso, viene visualizzato un elenco di studenti, le operazioni implicite SQL vengono eseguite per ogni studente. Ciò può essere, da un lato, superfluo e, dall’altro, sicuramente dispendioso in termini di tempo di esecuzione.
Per eseguire lo script con una base PostgreSQL, si crea la seguente configurazione di esecuzione:

L’esecuzione fornisce gli stessi risultati ottenuti con MySQL.
19.6.9. Lo script [main_stats_for_élève]

Lo script [main_stats_for_élève] è quello già utilizzato nell’applicazione |troiscouches v01]. All’epoca si chiamava [main]. Si tratta di un’applicazione da console che consente di ottenere alcuni indicatori relativi ai voti di uno studente: [moyenne pondérée, min, max, liste]. Si inserisce nella seguente architettura:

In questa architettura a livelli, solo il livello [dao] è stato modificato tra l'applicazione |troiscouches v01| e questa. Poiché il nuovo livello [dao] rispetta l'interfaccia [InterfaceDao] del precedente livello [dao], i livelli [ui, métier] non devono essere modificati. È quindi possibile continuare a utilizzare quelle definite nell’applicazione |troiscouches v01|.
Lo script [main_stats_for_élève] implementa il livello [main] dello schema sopra riportato nel modo seguente:
# si attende un parametro mysql o 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()
# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})
# il syspath è configurato - è possibile eseguire le importazioni
from MyException import MyException
# il livello [ui]
ui = config["ui"]
try:
# Esecuzione del livello [ui]
ui.run()
except MyException as ex1:
# viene visualizzato l'errore
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# viene visualizzato l'errore
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# si liberano le risorse
import shutdown
shutdown.execute(config)
- riga 20: si recupera un riferimento al livello [ui] nella configurazione dell’applicazione;
- riga 24: si avvia la finestra di dialogo con l'utente utilizzando l'unico metodo del livello [ui];
Una configurazione di esecuzione per PostgreSQL sarebbe la seguente:

Ecco un esempio di esecuzione con questa configurazione:
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