19. Uso de ORM SQLALCHEMY
En el capítulo anterior se mostró que, en ciertos casos, se puede escribir código independiente del SGBD, utilizado con la siguiente arquitectura:

En este capítulo utilizaremos el ORM (mapeador objeto-relacional) [sqlalchemy] para acceder a los SGBD de manera uniforme, independientemente del SGBD que se utilice. Un ORM permite dos cosas:
- permite que un script se comunique con el SGBD sin emitir órdenes SQL;
- oculta al script las particularidades de cada SGBD;
La arquitectura queda así:
El script ahora está separado de los conectores por el ORM. Se comunica con el ORM mediante clases y métodos. No ejecuta código SQL. Es el ORM el que lo hace con los conectores a los que está vinculado. Oculta al script las particularidades de estos conectores. Por lo tanto, el código del script no se ve afectado por un cambio de conector (es decir, del SGBD);
La estructura de los scripts analizados será la siguiente:

19.1. Instalación del ORM [sqlalchemy]
El ORM [sqlalchemy] viene en forma de un paquete de Python que hay que instalar en un terminal de 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. Scripts 01: lo básico

- en [1], los scripts que se analizarán. Estos scripts utilizarán las clases de [2]: BaseEntity, MyException, Persona, Utilidades;
19.2.1. Configuración
El archivo [config] configura la aplicación de la siguiente manera:
def configure():
# root_dir
# ruta absoluta de referencia de las rutas relativas de la configuración
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# rutas absolutas de las dependencias
absolute_dependencies = [
# BaseEntity, MyException, Persona, Herramientas
f"{root_dir}/classes/02/entities",
]
# se establece la ruta del sistema
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Configuración de las clases
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# se aplica la configuración
return {}
Comentarios
- línea 8: se agrega a la ruta de Python la carpeta que contiene las clases [BaseEntity, MyException, Personne, Utils];
- líneas 12-13: se establece la ruta de Python de la aplicación;
- líneas 16-17: tal vez recuerdes que la clase |BaseEntity| tiene un atributo de clase llamado [excluded_keys]. Este atributo es una lista en la que se incluyen las propiedades de la clase que no queremos que aparezcan en su diccionario (función asdict). Aquí excluimos la propiedad [_sa_instance_state] del estado de la clase [Personne]. Pronto veremos por qué;
19.2.2. Script [démo]
El script [démo] muestra un primer uso de ORM y [sqlalchemy]:
# se recupera la configuración de la aplicación
import config
config = config.configure()
# importaciones
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper
from Personne import Personne
# metadatos
metadata = MetaData()
# la tabla
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 clase Persona antes de la asignación
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")
# el mapeo
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nombre': personnes_table.c.prenom,
'apellido': personnes_table.c.nom,
'edad': personnes_table.c.age
})
# la persona 1 no ha sido modificada
print(f"personne1={personne1.__dict__}")
# la clase Persona sí se modificó: se «enriqueció»
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")
Comentarios
- líneas 1-4: se configura la aplicación;
- líneas 6-10: se importan los módulos necesarios para el script;
- línea 13: [MetaData] es una clase de [sqlalchemy];
- líneas 15-22: [Table] es una clase de [sqlalchemy]. Permite describir una tabla de una base de datos. Aquí vamos a describir la tabla [personnes] de la base de datos MySQL [dbpersonnes] estudiada en el capítulo |MySQL|;
- línea 16: el primer parámetro, [personnes], es el nombre de la tabla descrita;
- línea 16: el segundo parámetro [metadata] es la instancia [MetaData] creada en la línea 13;
- líneas 17-22: cada uno de los siguientes parámetros describe una columna de la tabla con una sintaxis propia de [sqlalchemy], pero similar a la sintaxis de SQL;
- cada columna se describe con una instancia de la clase [Column] de [sqlalchemy];
- el primer parámetro es el nombre de la columna;
- el segundo parámetro es su tipo;
- los siguientes parámetros son parámetros con nombre:
- línea 17: [primary_key=True] para indicar que la columna [id] es la clave primaria de la tabla [personnes];
- línea 18: [nullable=False] para indicar que una columna debe tener necesariamente un valor cuando se inserta una fila en la tabla;
- línea 21: por último, la clase [UniqueConstraint] permite describir una restricción de unicidad. Aquí se indica que las columnas (apellido, nombre) deben ser únicas en la tabla. La propiedad denominada [name] permite asignar un nombre a esta restricción. En este caso, hay que distinguir dos situaciones:
- se describe una tabla existente. En ese caso, hay que buscar el nombre de la restricción en las propiedades de la tabla (phpMyAdmin o pgAdmin);
- se describe una tabla que se va a crear. En ese caso, se le asigna el nombre que se desee;
- líneas 23-25: creamos una persona [personne1] y mostramos su diccionario [__dict__]. Aquí tendremos:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- líneas 27-33: se realiza una asignación, es decir, se establece una correspondencia entre la clase [Personne] y la tabla [personnes]. Se trata, esencialmente, de una correspondencia [propriétés de la classe colonnes de la table]. La función [mapper] acepta aquí tres parámetros:
- línea 28: el primer parámetro es el nombre de la clase para la que se realiza la asignación;
- línea 28: el segundo parámetro es la tabla a la que se asociará. Se trata del objeto [Table] creado en la línea 16;
- línea 28: el tercer parámetro es un parámetro denominado [properties]. Se trata de un diccionario en el que las claves son las propiedades de la clase mapeada y los valores, las columnas de la tabla mapeada. Para designar la columna X de la tabla [personnes_table], se escribe [personnes_table.c.X];
- líneas 35-36: se vuelve a mostrar la persona [personne1] una vez realizada la asignación. Se observa que no ha cambiado:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- líneas 37-39: se crea una nueva persona [personne2] y se muestra. Entonces se obtiene la siguiente visualización:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
Se observa que el diccionario [__dict__] ha sido modificado profundamente:
- (continuación)
- aparece una nueva propiedad [_sa_instance_state]. Se observa que es un objeto de ORM [sqlalchemy];
- las demás propiedades han perdido el prefijo que indicaba a qué clase pertenecían;
Por lo tanto, podemos concluir que la operación de mapeo de las líneas 27 a 33 modificó la clase [Personne].
Cuando queramos mostrar el estado de un objeto [Personne], por lo general no nos interesará la propiedad [_sa_instance_state]. De hecho, esta solo está ahí para el funcionamiento interno de [sqlalchemy] y, por lo general, no nos interesa. Por eso se escribió en el script [config]:
# configuración de las clases
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
19.2.3. El script [main]
El script [main] manipulará la tabla [personnes] de la base de datos MySQL [dbpersonnes] al interactuar con [sqlalchemy]. Para entender lo que sigue, hay que recordar la arquitectura utilizada aquí:

Si [Database1] es la base [dbpersonnes], se observa que la conexión entre el script y esta base pasa por dos entidades:
- el conector de Python a SGBD MySQL;
- el SGBD MySQL;
El script [main] se comunicará con el ORM, el cual a su vez se comunicará con el conector de Python. El ORM se comunica con este conector mediante las herramientas descritas en los párrafos |MySQL| y |PostgreSQL|, en particular al emitir órdenes SQL. El script [main] no utilizará órdenes SQL. Se basará en la API (Interfaz de programación de aplicaciones) de la ORM, compuesta por clases e interfaces.
El script [main] es el siguiente:
# se configura la aplicación
import config
config = config.configure()
# importaciones
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
# cadena de conexión a una base de datos MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadatos
metadata = MetaData()
# la tabla
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 asignación
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nombre': personnes_table.c.prenom,
'apellido': personnes_table.c.nom,
'edad': personnes_table.c.age
})
# la fábrica de sesiones
Session = sessionmaker()
Session.configure(bind=engine)
session = None
try:
# una sesión
session = Session()
# eliminación de la tabla [personnes]
session.execute("drop table if exists personnes")
# recreación de la tabla a partir de la asignación
metadata.create_all(engine)
# una inserción
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
# una consulta
personnes = session.query(Personne).all()
# visualización
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# otras dos inserciones, de las cuales la segunda falla debido a la regla de unicidad (nombre, apellido)
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 consulta
personnes = session.query(Personne).all()
# visualización
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# validación de la sesión
session.commit()
except (InterfaceError, IntegrityError) as erreur:
# visualización
print(f"L'erreur suivante s'est produite : {erreur}")
# cancelación de la última sesión
if session:
print("rollback...")
session.rollback()
finally:
# se liberan los recursos de la sesión
if session:
session.close()
Comentarios
- líneas 1-4: se configura la aplicación;
- líneas 7-9: se importan una serie de clases e interfaces de la biblioteca [sqlalchemy];
- línea 11: se importa la clase [Personne];
- línea 14: la cadena de conexión a la base de datos. Especifica:
- el SGBD utilizado (mysql);
- el conector de Python utilizado (mysql.connector sin el punto);
- el usuario que se conecta (admpersonnes);
- su contraseña (nobody);
- la máquina en la que se encuentra el SGBD (localhost = máquina en la que se encuentra el script que se ejecuta);
- el nombre de la base de datos (dbpersonnes);
Con esta información, [sqlalchemy] puede conectarse a la base de datos. Cabe señalar que el conector de Python utilizado debe estar ya instalado. [sqlalchemy] no lo hace.
- líneas 19-26: descripción de la tabla [personnes];
- líneas 28-34: mapeo entre la clase [Personne] y la tabla [personnes];
- líneas 36-38: la mayoría de las operaciones [sqlalchemy] se realizan en una sesión. El concepto de sesión [sqlalchemy] es similar al de transacción SQL. Las sesiones se crean a partir de la clase [Session], devuelta por la función [sessionmaker] de la línea 37;
- línea 38: la clase [Session] se asocia a la base [dbpersonnes] a través de la cadena de conexión de la línea 14;
- línea 43: se crea una sesión. Como ya se mencionó, una sesión puede compararse con una transacción;
- líneas 45-46: el método [Session.execute] permite ejecutar una orden SQL. Esto no es algo habitual, ya que se ha mencionado que el ORM permite evitar el lenguaje SQL;
- líneas 48-49: el método [metadata.create_all] permite crear todas las tablas que utilizan la instancia [MetaData] de la línea 17. Solo tenemos una: la tabla [personnes] definida en las líneas 20-26. [sqlalchemy] utilizará la información de estas líneas para crear la tabla. Aquí tenemos una primera ventaja de ORM: oculta las particularidades de los SGBD. De hecho, la orden de SQL a [create] puede variar mucho de un SGBD a otro debido a los tipos asignados a las columnas. No ha habido una estandarización de los tipos de datos. Por lo tanto, el orden varía de un caso a otro. Aquí, gracias a esto:
- describimos de manera única la tabla que deseamos;
- [sqlalchemy] se encarga de generar el [create] adecuado para el SGBD que tiene frente a sí;
- línea 52: se agrega un objeto [Personne] a la sesión. Esto no lo agrega automáticamente a la base de datos. De hecho, un ORM sigue sus propias reglas para sincronizarse con la base de datos. Siempre buscará optimizar el número de consultas que realiza. Veamos un ejemplo. El script agrega (add) dos personas (persona1, persona2) a la sesión y luego realiza una consulta: quiere ver todas las personas que figuran en la tabla. [sqlalchemy] puede proceder de la siguiente manera:
- la adición de [personne1] puede realizarse en memoria. Por el momento, no es necesario agregarla a la base de datos;
- lo mismo ocurre con [personne2];
- luego viene la consulta de tipo [select]. En ese momento hay que recuperar todas las filas de la tabla [personnes]. [sqlalchemy] insertará entonces [personne1, personne2] en la base de datos y luego realizará la consulta;
[sqlalchemy] realizará así optimizaciones transparentes para el desarrollador.
- línea 56: para realizar una consulta del tipo [select] (quiero ver…), se utiliza el método [Session.query]. El parámetro del método [query] es la clase mapeada con la tabla consultada. Este método devuelve un tipo [Query]. El método [Query.all] solicita todos los objetos [Personne] de la sesión. Se le devuelven todas las filas de la tabla [personnes], cada una en forma de un objeto [Personne]. Para ello, [sqlalchemy] utiliza la asignación que se ha realizado entre la clase [Personne] y la tabla [personnes]. El resultado de la línea 56 es una lista de objetos [Personne];
- líneas 58-61: se muestran los elementos de la lista [personnes]. Dado que la clase [Personne] deriva de la clase [BaseEntity], el método [Personne.__str__] utilizado aquí implícitamente en la línea 61 es, en realidad, el método [BaseEntity.__str__], el cual devuelve la cadena jSON del objeto que realiza la llamada. Esta cadena es la cadena jSON del diccionario [Personne.asdict] (véase |BaseEntity|). Hemos dicho que, tras la asignación, encontraríamos la propiedad [_sa_instance_state] en cada objeto [Personne]. Sin embargo, el valor de esta propiedad no es de tipo [BaseEntity]. Por lo tanto, hay que excluirla del diccionario de la clase [Personne]; de lo contrario, la visualización se «congela». Esto es lo que se hizo en el script [config];
- líneas 63-65: se agregan otras dos personas que tienen el mismo nombre y apellido. Sin embargo, existe una restricción de unicidad en la unión de estas dos columnas. Por lo tanto, debería producirse un error. Eso es lo que queremos comprobar;
- líneas 67-68: se solicita nuevamente la lista de todas las personas de la base de datos;
- líneas 70-73: y las mostramos;
- líneas 75-76: la sesión se confirma («commit»). Como su nombre lo indica, la transacción subyacente se validará;
- veremos durante la ejecución que las líneas 67-76 no se ejecutarán debido a la excepción generada por la línea 65. A continuación, pasaremos a las líneas 78-84 para manejar la excepción;
- línea 78: la excepción [InterfaceError] se produce si [sqlalchemy] no logra conectarse a la base de datos [dbpersonnes]. La excepción [IntegrityError] se produce en la línea 65;
- línea 80: se muestra el error;
- líneas 82-84: si la sesión existe, se cancela. Esto equivale a cancelar la transacción subyacente;
- líneas 85-88: en todos los casos, haya error o no, se cierra la sesión para liberar recursos;
Los resultados de la ejecución son los siguientes:
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
- líneas 2-3: la lista de personas después de la primera inserción;
- línea 5: la excepción [IntegrityError] que se produjo al agregar dos personas con el mismo nombre y apellido;
- líneas 6-7: cabe destacar la orden SQL que falló. Se trata de una orden INSERT configurada: [sqlalchemy] insertó a las dos personas con una sola orden INSERT. Aquí vemos que intentó optimizar las órdenes SQL emitidas;
Ahora veamos, con phpMyAdmin, el contenido de la tabla [personnes]:

En [6] vemos que la tabla está vacía. Ni siquiera aparece la primera persona que el script había agregado a la sesión. Esto se debe a que la sesión se realizaba dentro de una transacción y esta fue revertida en la cláusula [except] del script [main].
Ahora realicemos la siguiente modificación en [main]:
# una inserción
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
Después de agregar una persona en la línea 2, descomentamos la línea 3. La operación [session.commit] validará la transacción subyacente y se iniciará una nueva transacción. Tras la ejecución, el contenido de la tabla [personnes] es el siguiente:

En [6] se observa que se conservó la primera inserción. Esto se debe a que se realizó dentro de una transacción 1 y el error posterior ocurrió dentro de una transacción 2.
19.3. Scripts 02: las asignaciones de [sqlalchemy]

Los scripts 02 son una variante de los scripts 01. Se intenta realizar el mayor número posible de configuraciones en [config.py]. Ahora se configura allí el entorno [sqlalchemy] de la aplicación:
def configure():
# ruta absoluta de referencia de las rutas relativas de la configuración
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# rutas absolutas de las dependencias
absolute_dependencies = [
# BaseEntity, MyException, Persona, Utilidades
f"{root_dir}/classes/02/entities",
]
# se establece la ruta del sistema
from myutils import set_syspath
set_syspath(absolute_dependencies)
# importaciones
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper, sessionmaker
# enlace a una base de datos MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# metadatos
metadata = MetaData()
# la tabla
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 asignación
from Personne import Personne
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'nombre': personnes_table.c.prenom,
'apellido': personnes_table.c.nom,
'edad': personnes_table.c.age
})
# la fábrica de sesiones
Session = sessionmaker()
Session.configure(bind=engine)
# esta información se incluye en la configuración
config = {}
config["Session"] = Session
config["metadata"] = metadata
config["engine"] = engine
config["personnes_table"] = personnes_table
# configuración de las clases
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# se aplica la configuración
return config
Comentarios
- líneas 2-12: configuración de la ruta de Python;
- líneas 14-45: se configura el entorno [sqlalchemy];
- líneas 47-52: se agrega el entorno [sqlalchemy] al diccionario de configuración;
- líneas 54-56: se configura la clase [Personne];
Con esta configuración, el script [main] queda así:
# se configura la aplicación
import config
config = config.configure()
# El syspath está configurado; se realizan las importaciones
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError
from Personne import Personne
session = None
try:
# una sesión
session = config["Session"]()
# se elimina la tabla [personnes]
session.execute("drop table if exists personnes")
# recreación de la tabla a partir de la asignación
config["metadata"].create_all(config["engine"])
# dos inserciones
session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
session.add(personne)
# validación de las dos inserciones
session.commit()
# una consulta
personnes = session.query(Personne).all()
# visualización
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# otras dos inserciones, de las cuales la segunda falla
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
# una consulta
personnes = session.query(Personne).all()
# visualización
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# validación de la sesión
session.commit()
except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
# visualización
print(f"L'erreur suivante s'est produite : {erreur}")
# cancelación de la última sesión
if session:
print("rollback...")
session.rollback()
finally:
# visualización
print("Travail terminé...")
# se liberan los recursos de la sesión
if session:
session.close()
Los resultados de la ejecución son los siguientes:
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
En phpMyAdmin, la tabla [personnes] quedó así:

Ahora, veamos la tabla [personnes] generada por [sqlalchemy]:

- en [6], los tipos utilizados para las distintas columnas;
- en [7], vemos que la columna [id] tiene el atributo [AUTO_INCREMENT]. Esto significa que al insertar una fila en la tabla, si esa fila no tiene un valor para la columna [id], este será generado por MySQL de manera incremental: 1, 2, 3, … Esta propiedad nos evita tener que preocuparnos por el valor de la clave primaria al realizar una inserción en la tabla: dejamos que MySQL la genere;
- en [8], vemos que la columna [id] es la clave primaria;
- en [9], encontramos la restricción de unicidad en los campos [nom, prenom];
19.4. Scripts 03: manipulación de las entidades de la sesión [sqlalchemy]

El archivo de configuración [config] es el mismo que en el ejemplo anterior. En el script [main] se realizan las operaciones clásicas de [INSERT, UPDATE, DELETE, SELECT] en la tabla [personnes] utilizando los métodos de [sqlalchemy]:
# se configura la aplicación
import config
config = config.configure()
# importaciones
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne
# muestra el contenido de la tabla [personnes]
def affiche_table(session: Session):
print("----------------")
# una consulta
personnes = session.query(Personne).all()
# visualización
affiche_personnes(personnes)
# muestra una lista de personas
def affiche_personnes(personnes: list):
print("----------------")
# visualización
for personne in personnes:
print(personne)
# principal ---------------------------
session = None
try:
# una sesión
session = config["Session"]()
# eliminación de la tabla [personnes]
# checkfirst=True: primero verifica que la tabla exista
config["personnes_table"].drop(config["engine"], checkfirst=True)
# recreación de la tabla a partir de la asignación
config["metadata"].create_all(config["engine"])
# inserciones
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}))
# se muestra el contenido de la sesión
affiche_table(session)
# lista de personas en orden alfabético por apellido y, en caso de igual apellido, en orden alfabético por nombre
personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())
# visualización
affiche_personnes(personnes)
# lista de personas cuya edad se encuentra en el intervalo [20,40], ordenadas de mayor a menor según la edad
# y, a continuación, en caso de igualdad de edad, por orden alfabético de los apellidos y, en caso de igualdad de apellidos, por orden alfabético de los nombres
personnes = session.query(Personne). \
filter(Personne.âge >= 20, Personne.âge <= 40). \
order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())
# visualización
affiche_personnes(personnes)
# inserción de la Sra. Bruneau
bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
session.add(bruneau)
# modificación de su edad
bruneau.âge = 47
# lista de personas con el apellido Bruneau
personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()
# visualización
affiche_personnes([personne])
# eliminación de la Sra. Bruneau
session.delete(personne)
# lista de personas con el apellido Bruneau
personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")
# visualización
affiche_personnes(personnes)
# validación de la sesión
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# visualización
print(f"L'erreur suivante s'est produite : {erreur}")
# cancelación de la última sesión
if session:
session.rollback()
finally:
# visualización
print("Travail terminé...")
# se liberan los recursos de la sesión
if session:
session.close()
Comentarios
- líneas 20-25: la función [affiche_personnes] muestra los elementos de una lista de personas;
- líneas 12-18: la función [affiche_table] muestra el contenido de la tabla [personnes];
- líneas 34-36: se elimina la tabla [personnes]. A diferencia de las versiones anteriores, no se utiliza una orden SQL, sino un método de [sqlalchemy]:
- config["personnes_table"] es el objeto [Table] que describe la tabla [personnes];
- config["engine"] es la cadena de conexión a la base de datos [dbpersonnes];
- el parámetro denominado [checkfirst=True] especifica que la operación solo se realice si existe la tabla [personnes];
- líneas 38-39: se vuelve a crear la tabla [personnes];
- líneas 41-44: se agregan tres personas a la sesión. Cabe recordar que no necesariamente se insertan de inmediato en la tabla [personnes]. Esto depende de la estrategia de [sqlalchemy], que tiene como objetivo el rendimiento;
- líneas 46-47: se muestra el contenido de la tabla [personnes]. Si aún no se habían realizado las inserciones de las tres personas, ahora se realizan debido a esta solicitud;
- líneas 49-50: un ejemplo de uso del método [order_by], que permite presentar los resultados de una consulta en un orden determinado. La sintaxis [order_by(critère1, critère2)] muestra los resultados primero según el criterio [critère1] y, cuando las filas presentan el mismo valor de [critère1], se ordenan según el criterio [critère2]. Se pueden establecer varios criterios de esta manera;
- líneas 55-59: introducen el concepto de filtro con el método [filter]. La notación [filter(critère1, critère2)] establece una relación lógica (AND) entre los criterios utilizados;
- líneas 64-67: se inicia una nueva sesión para una persona;
- líneas 70-71: otro ejemplo de consulta filtrada. La función [func.lower(param)] convierte [param] a minúsculas. Existen otras funciones disponibles, como [func.xx]. En la expresión de la línea 71:
- [session.query.filter] devuelve una lista de objetos [Personne];
- [session.query.filter.first] devuelve el primer elemento de esta lista;
- línea 77: se elimina un elemento de la sesión;
- línea 86: se valida la sesión;
Los resultados de la ejecución son los siguientes:
- líneas 4-6: el contenido de la sesión;
- líneas 8-10: el contenido de la sesión en orden descendente por nombre;
- líneas 12-13: el contenido de la sesión para las personas cuya edad se encuentra en el intervalo [20, 40];
- línea 15: la persona llamada «bruneau»;
En phpMyAdmin, el contenido de la tabla [personnes] al finalizar la ejecución es el siguiente:

19.5. Scripts 04: uso de una base de datos [PostgreSQL]

El archivo [04] es una copia del archivo [03]. Solo se modifica una cosa: la cadena de conexión en el archivo [config]:
# enlace a una base de datos PostgreSQL
engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")
Ahora esta cadena de conexión apunta a la base de datos [dbpersonnes] de un SGBD [PostgreSQL]. Cabe destacar el uso del conector [psycopg2]. Este debe estar instalado.
Al ejecutar el script [main] se obtienen los siguientes resultados:
Con la herramienta [pgAdmin] (véase el párrafo |pgAdmin|), la tabla [personnes] se encuentra en el siguiente estado:

La tabla [personnes] se generó con el siguiente código SQL:

- En [4-5], se observa que la columna [id] es la clave primaria. También se observa que tiene un valor por defecto [mot clé DEFAULT], lo que hace que, si se inserta una fila sin clave primaria, esta sea generada por el SGBD. Este es un funcionamiento común: se deja que el SGBD genere las claves primarias;
Esta versión 05 de los scripts [sqlalchemy] muestra claramente lo fácil que es pasar de un SGBD a otro: solo hubo que cambiar la cadena de conexión en un script de configuración. Nada más ha cambiado. Si comparamos los tipos de las columnas del [id, nom, prenom, age] anterior con los de la tabla MySQL del ejemplo |02|, vemos que son diferentes. [sqlalchemy] los adapta al SGBD utilizado. Esta facilidad para adaptarse a un nuevo SGBD es razón suficiente para adoptar [sqlalchemy] u otro ORM.
19.6. Scripts 05: ejemplo completo

El ejemplo que analizamos es una versión del que se estudió en el párrafo |troiscouches-v01|. Ese ejemplo presentaba una arquitectura de tres capas [ui, métier, dao] que manipulaba entidades [Classe, Elève, Matière, Note]. Las entidades estaban codificadas de forma estática en una capa [dao]. Ahora las colocaremos en una base de datos. Utilizaremos dos SGBD: MySQL y PostgreSQL.
19.6.1. La arquitectura de la aplicación
La arquitectura de la aplicación será la siguiente:

- En [1-3], se encuentran las capas [ui, métier, dao] que ya están presentes en el ejemplo |troiscouches-v01|. La capa [dao] ahora se comunica con la capa [ORM];
- las capas [1-5] están implementadas mediante código en Python;
19.6.2. Las bases de datos
Creamos una base de datos MySQL llamada [dbecole], propiedad del usuario [admecole], con la contraseña [mdpecole]. Para ello, seguimos el procedimiento descrito en el apartado |creación de una base de datos|:


- en [1], la base [dbecole] sin las tablas de [3];
- en [7], el usuario [admecole] tiene todos los privilegios sobre esta base de datos;
Hacemos lo mismo con SGBD y PostgreSQL. Creamos una base de datos llamada [dbecole], propiedad del usuario [admecole], con la contraseña [mdpecole]. Para ello, seguimos el procedimiento descrito en el apartado |creación de una base de datos|:

- en [1], la base [dbecole];
- en [2], el usuario [admecole];
- en [3-4], la base [dbecole] es propiedad del usuario [admecole];
19.6.3. Las entidades manipuladas por la aplicación
En la aplicación |troiscouches v01|, las entidades manejadas eran las siguientes (véase |entidades|). Son estas entidades las que se almacenarán en las bases de datos anteriores. No duplicaremos estas entidades en la nueva aplicación. Las obtendremos de donde ya están definidas.
La clase [Classe]:
# importaciones
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# atributos excluidos del estado de la clase
excluded_keys = []
# propiedades de la clase
@staticmethod
def get_allowed_keys() -> list:
# id: identificador de la clase
# nombre: nombre de la clase
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):
# nombre debe ser una cadena de caracteres no vacía
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 clase [Elève]:
# importaciones
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# atributos excluidos del estado de la clase
excluded_keys = []
# propiedades de la clase
@staticmethod
def get_allowed_keys() -> list:
# id: identificador del alumno
# apellido: apellido del alumno
# nombre: nombre del alumno
# clase: clase del alumno
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:
# el apellido debe ser una cadena de caracteres no vacía
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:
# nombre debe ser una cadena de caracteres no vacía
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:
# se espera un tipo Clase
if isinstance(value, Classe):
self.__classe = value
# o un tipo «dict»
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# o 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 clase [Matière]:
# importaciones
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# atributos excluidos del estado de la clase
excluded_keys = []
# propiedades de la clase
@staticmethod
def get_allowed_keys() -> list:
# id: identificador de la materia
# nombre: nombre de la materia
# coeficiente: coeficiente de la 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
# setters
@nom.setter
def nom(self: object, nom: str):
# el nombre debe ser una cadena de caracteres no vacía
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):
# el coeficiente debe ser un número real >=0
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# ¿Error?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
La clase [Note]:
# importaciones
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# atributos excluidos del estado de la clase
excluded_keys = []
# propiedades de la clase
@staticmethod
def get_allowed_keys() -> list:
# id: identificador de la calificación
# valor: la calificación en sí
# alumno: alumno (del tipo Alumno) al que se refiere la calificación
# materia: materia (de tipo Materia) a la que corresponde la nota
# el objeto «Calificación» es, por lo tanto, la calificación de un alumno en una materia
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
# getter
@valeur.setter
def valeur(self: object, valeur: float):
# la nota debe ser un número real entre 0 y 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:
# se espera un tipo «Alumno»
if isinstance(value, Elève):
self.__élève = value
# o un tipo «dict»
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# o 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:
# se espera un tipo «Asignatura»
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. Configuración

La configuración se ha dividido en varios archivos:
- la configuración general en [config.py]: establece la ruta de Python de la aplicación e instancia las capas de la arquitectura;
- la configuración de [sqlalchemy] en [config_database]: realiza las asignaciones entre clases y tablas;
- las capas de la aplicación se configuran en [config_layers];
El archivo [config] es el siguiente:
def configure(config: dict) -> dict:
import os
# paso 1 ---
# se establece la ruta de Python de la aplicación
# ruta absoluta de la carpeta de este script
script_dir = os.path.dirname(os.path.abspath(__file__))
# ruta absoluta de referencia para las rutas relativas de la configuración
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# rutas absolutas de las dependencias
absolute_dependencies = [
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# proyecto de tres capas v01
f"{root_dir}/troiscouches/v01/interfaces",
f"{root_dir}/troiscouches/v01/services",
f"{root_dir}/troiscouches/v01/entities",
# documentación del presente proyecto
script_dir,
f"{script_dir}/../services",
]
# actualización de syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# paso 2 ------
# Configuración de la base de datos
import config_database
config = config_database.configure(config)
# paso 3 ------
# instanciación de las capas de la aplicación
import config_layers
config = config_layers.configure(config)
# se aplica la configuración
return config
- líneas 4-27: construcción de la ruta de Python de la aplicación;
- líneas 29-32: configuración de [sqlalchemy];
- líneas 34-37: configuración de las capas de la aplicación;
El archivo [config_database] es el siguiente:
def configure(config: dict) -> dict:
# config['sgbd'] es el nombre del SGBD utilizado
# MySQL: MySQL
# pgres: PostgreSQL
# configuración de SQLAlchemy
from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine
from sqlalchemy.orm import mapper, relationship, sessionmaker
# cadenas de conexión a las bases de datos en uso
engines = {
'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
}
# cadena de conexión a la base de datos en uso
engine = create_engine(engines[config['sgbd']])
# metadatos
metadata = MetaData()
# las tablas de la base de datos
tables = {}
# las clases mapeadas
from Classe import Classe
from Elève import Elève
from Note import Note
from Matière import Matière
# la tabla de clases
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,
'nombre': classes_table.c.nom
})
# la tabla de alumnos
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),
# un alumno pertenece a una clase
Column('classe_id', Integer, ForeignKey('classes.id')),
)
# correspondencia
mapper(Elève, tables['élèves'], properties={
'id': élèves_table.c.id,
'apellido': élèves_table.c.nom,
'nombre': élèves_table.c.prénom,
'clase': relación(Clase, backref="alumnos", lazy="seleccionar")
})
# el índice
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)
)
# asignación
mapper(Matière, tables['matières'], properties={
'id': matières_table.c.id,
'nombre': matières_table.c.nom,
"coefficient": matières_table.c.coefficient
})
# la tabla de calificaciones
tables['notes'] = notes_table = \
Table("notes", metadata,
Column('id', Integer, primary_key=True),
Column('valeur', Float, nullable=False),
# una nota es la de un alumno
Column('élève_id', Integer, ForeignKey('élèves.id')),
# una nota corresponde a una materia
Column('matière_id', Integer, ForeignKey('matières.id')),
)
# correspondencia
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'valor': notes_table.c.valeur,
'alumno': relación(Alumno, backref="notas", lazy="seleccionar"),
'asignatura': relación(Asignatura, backref="notas", lazy="seleccionar")
})
# configuración de las entidades [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 fábrica de sesiones
Session = sessionmaker()
Session.configure(bind=engine)
# una sesión
session = Session()
# se registra cierta información en el diccionario de configuración
config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
# se devuelve la configuración
return config
Comentarios
- líneas 1-4: la función [configure] recibe un diccionario como parámetro. Solo se utiliza la clave [sgbd]. Su valor es [mysql] si la base es una base MySQL, y [pgres] si la base es una base PostgreSQL;
- líneas 6-9: importaciones de elementos de [sqlalchemy]. El script [config_database] realiza las correspondencias entre las tablas de la base de datos [dbecole] y las entidades [Classes, Elève, Matière, Note]. En la tabla, los datos de la entidad están encapsulados en una fila. En el código de Python, se encapsulan en un objeto. De ahí el nombre ORM (Object Relational Mapper): el ORM establece una correspondencia (una relación) entre las filas de una base de datos relacional y los objetos. En esta aplicación, tenemos cuatro entidades [Classe, Elève, Matière, Note] que se vincularán a cuatro tablas [classes, élèves, matières, notes]. Tenga en cuenta que los nombres de las tablas pueden contener caracteres acentuados;
- líneas 11-17: la cadena de conexión a la base de datos utilizada. Esta depende del elemento config[‘sgbd’];
- líneas 24-28: las entidades de la aplicación que serán objeto de una asignación [sqlalchemy]. Cuando se ejecuten estas líneas, la ruta de Python ya habrá sido establecida por el script [config];
- líneas 30-40: la asignación entre la entidad [Classe] y la tabla [classes];
- líneas 30-35: la tabla [classes] se define con la clase [Table] de [sqlalchemy]. Indicamos que esta tabla tiene dos columnas:
- la columna [id], que es la clave primaria y corresponde al número de la clase, línea 33;
- la columna [nom], que contiene el nombre de la clase, línea 34;
- líneas 31-32: ten en cuenta que la sintaxis x=y=z es válida en Python: el valor de z se asigna a y y luego el valor de y a x;
- líneas 37-40: se enumeran las correspondencias entre las columnas de la tabla [classes] y las propiedades de la entidad [Classe];
- líneas 42-57: la asignación entre la entidad [Elève] y la tabla [élèves];
- líneas 51-57: la tabla [élèves] se define con la clase [Table] de [sqlalchemy]. Indicamos que esta tabla tiene cuatro columnas:
- la columna [id], que es la clave primaria y corresponde al número de estudiante, línea 45;
- la columna [nom], que contiene el apellido del alumno, línea 46;
- la columna [prénom], que contiene el nombre del alumno, línea 47. Tenga en cuenta que el nombre de una columna puede incluir caracteres acentuados;
- línea 49, la columna [classe_id], que contendrá el número de la clase a la que pertenece el alumno. A esto se le llama una clave externa. [élèves.classe_id] es una clave externa (ForeignKey) sobre la columna [classes.id]. Esto significa que el valor de [élèves.classe_id] debe existir en la columna [classes.id];
- líneas 51-57: se enumeran las correspondencias entre las columnas de la tabla [élèves] y las propiedades de la entidad [Elève]:
- las líneas 53-55 son fáciles de entender;
- la línea 56 es más complicada: define el valor de la propiedad [Elève.classe] como calculado mediante la relación de clave externa que vincula las tablas [élèves] y [classes]. Los parámetros de la función [relationship] son los siguientes:
- [Classe]: es el nombre de la entidad con la que la entidad [Elève] tiene una relación de clave externa. Esto debe reflejarse en la tabla [élèves] mediante la presencia de una clave externa en la tabla [classes]. Sabemos que esta existe;
- [backref="élèves"]: el nombre de una propiedad que se agregará a la entidad [Classe]. [Classe.élèves] será la lista de todos los alumnos de la clase. Esta propiedad no debe existir ya. Si ya existe, simplemente hay que elegir otro nombre aquí para [backref]. El desarrollador no tiene que gestionar esta propiedad. [sqlalchemy] se encargará de ello. Solo debe saber que existe, que fue agregada por [sqlalchemy], y que puede utilizarla en su código;
- [lazy=’select’]: esto significa que ORM no debe intentar asignar un valor de inmediato a la propiedad [Elève.classe]. Solo debe buscar su valor cuando el código lo solicite explícitamente. Por lo tanto:
- si el código solicita la lista de todos los alumnos, estos se mostrarán, pero su propiedad [classe] no se calculará;
- un poco más tarde, el código se interesa por un alumno específico [e] y hace referencia a su clase [e.classe]. Esta referencia obligará entonces a [sqlalchemy] a realizar una consulta a la base de datos para recuperar la clase del alumno, de manera transparente para el desarrollador;
- además, la inclusión de [lazy=’select’] tiene como objetivo evitar consultas innecesarias a la base de datos;
- línea 56: cuando ORM recupera una fila de la tabla [élèves], obtiene la información de [id, nom, prénom, classe_id]. A partir de ahí, debe construir un objeto Alumno(id, apellido, nombre, clase). En el caso de las propiedades [id, nom, prénom], esto no presenta dificultades. Sin embargo, para la propiedad [classe], es más complicado. Su valor es una referencia a un objeto de tipo [Classe]. Sin embargo, el objeto ORM solo contiene una información: [élèves.classe_id]. Como [élèves.classe_id] es una clave externa en la columna [classes.id], aquí se le indica que utilice esta relación para recuperar de la tabla [classes] la fila con id=[élèves.classe_id] (que necesariamente existe) y que, a partir de esa fila, cree el objeto [Classe] que espera la propiedad [Elève.classe];
- líneas 59-71: la asignación entre la entidad [Matière] y la tabla [matières];
- líneas 59-65: definición de la tabla [sqlalchemy] denominada [matières];
- líneas 66-71: se enumeran las correspondencias entre las columnas de la tabla [matières] y las propiedades de la entidad [Matière]. Aquí no hay dificultades;
- líneas 73-90: la asignación entre la entidad [Note] y la tabla [notes];
- líneas 73-82: definición de la tabla [sqlalchemy] denominada [notes]. Tiene dos claves externas:
- línea 79, la columna [notes.élève_id] toma sus valores de la columna [élèves.id]]. Esta clave externa refleja el hecho de que una nota pertenece a un alumno específico;
- línea 81, la columna [notes.matière_id] toma sus valores de la columna [matières.id]. Esta clave externa representa el hecho de que una calificación es una calificación en una materia específica;
- líneas 84-90: la correspondencia entre la entidad [Note] y la tabla [notes]:
- línea 88: la propiedad [Note.élève] debe tener como valor una instancia del tipo [Elève]. La tabla ORM solo contiene, en la fila de la tabla [notes], la columna [notes.élève_id], que hace referencia a la columna [élèves.id]. Aquí se indica que se utilice esta relación de clave externa para recuperar la instancia [Elève] de la cual se tiene la nota. Además, [relationship(Elève, backref="notes", …)] creará la nueva propiedad [Elève.notes], que será la lista de calificaciones del alumno. Esta propiedad no debe existir ya en la clase [Elève];
- línea 89: la propiedad [Note.matière] debe tener como valor una instancia de tipo [Matière]. La tabla ORM solo contiene, en la fila de la tabla [notes], la columna [notes.matière_id], que hace referencia a la columna [matières.id]. Aquí se indica que se utilice esta relación de clave externa para recuperar la instancia [Matière] de la que se tiene la nota. Por otra parte, [relationship(Matière, backref="notes", …)] creará la nueva propiedad [Matière.notes], que será la lista de notas de la asignatura. Esta propiedad no debe existir ya en la clase [Matière];
- líneas 92-96: se define, para cada entidad derivada de [BaseEntity], la lista de propiedades que se deben excluir del diccionario de propiedades de la entidad (BaseEntity.asdict). Hemos visto que [sqlalchemy] agregaba la propiedad [_sa_instance_state] a todas las entidades mapeadas. No queremos que aparezca en el diccionario de propiedades. Además, hemos visto que los mapeos anteriores habían agregado nuevas propiedades a las entidades:
- [Elève.notes]: todas las calificaciones del alumno;
- [Classe.élèves]: todos los alumnos de la clase;
- [Matière.notes]: todas las calificaciones de la materia;
En general, no queremos que estas propiedades se incluyan en el estado de la entidad. De hecho, calcular su valor tiene un costo SQL y este valor suele ser innecesario. Así, si recuperamos al alumno llamado «X»:
- (continuación)
- el ORM devolverá una entidad [Elève(id, nom, prénom, classe, notes)]. Debido a [lazy=’select’], las propiedades [classe, notes] vinculadas a claves externas de la base de datos no se habrán calculado;
- ahora bien, si muestro la cadena jSON de este alumno, sabemos que será la cadena jSON del diccionario [asdict] de la entidad. Si las propiedades [classe] y [notes] están ahí, [sqlalchemy] se verá obligado a realizar consultas a SQL para calcular sus valores. Esto es costoso. Si se pueden evitar estas consultas, es preferible;
- aquí hemos excluido todas las propiedades vinculadas a una clave externa;
- líneas 98-100: instanciación y configuración de un [Session factory] (factory = fábrica de producción). El objeto [Session] sirve para crear sesiones [sqlalchemy] respaldadas por transacciones;
- líneas 102-103: creación de una sesión sqlalchemy];
- línea 106: algunos elementos de la configuración [sqlalchemy] se colocan en el diccionario global de configuración de la aplicación;
- línea 109: se devuelve este diccionario;
El archivo [config_layers] configura las capas de la aplicación:
def configure(config: dict) -> dict:
# instanciación de la capa [dao]
from DatabaseDao import DatabaseDao
dao = DatabaseDao(config)
# instanciación de la capa [métier]
from Métier import Métier
métier = Métier(dao)
# instanciación de la capa [ui]
from Console import Console
ui = Console(métier)
# se agregan las capas a la configuración
config['dao'] = dao
config['métier'] = métier
config['ui'] = ui
# se genera la configuración
return config
- línea 1: la función [configure] recibe el diccionario de la configuración global de la aplicación;
- líneas 2-12: se instancian las capas de la aplicación;
- líneas 15-17: las referencias de las capas se incluyen en la configuración global;
- línea 20: se devuelve la nueva configuración;
19.6.5. La capa [dao] - 1

Hay que entender aquí que la capa [dao] [3] se comunica con laORM [sqlalchemy] [4], configurada tal como se describió en el párrafo anterior. De las tres capas [ui, métier, dao] de la aplicación |troiscouches v01|, solo se debe reescribir la capa [dao]. Las capas [ui, métier] se conservan.
La implementación de la capa [dao] se ha colocado en la carpeta [services]:

[InterfaceDatabaseDao] es la interfaz de la capa [dao]:
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
class InterfaceDatabaseDao(InterfaceDao, ABC):
# inicialización de la base de datos
@abstractmethod
def init_database(self, data: dict):
pass
- línea 6: la interfaz [InterfaceDatabaseDao] deriva tanto de la clase [ABC], para ser una clase abstracta, como de la interfaz [InterfaceDao] del proyecto |troiscouches v01|;
- líneas 8-11: se agrega el método [init_database] a los métodos heredados de [InterfaceDao]. Su función será inicializar la base de datos con los datos del diccionario [data] que se le pasan como parámetro en la línea 10;
Recordemos que la interfaz [InterfaceDao] era la siguiente:
# importaciones
from abc import ABC, abstractmethod
# interfaz DAO
from Elève import Elève
class InterfaceDao(ABC):
# lista de clases
@abstractmethod
def get_classes(self: object) -> list:
pass
# lista de alumnos
@abstractmethod
def get_élèves(self: object) -> list:
pass
# lista de materias
@abstractmethod
def get_matières(self: object) -> list:
pass
# lista de calificaciones
@abstractmethod
def get_notes(self: object) -> list:
pass
# lista de calificaciones de un alumno
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# buscar un alumno por su ID
@abstractmethod
def get_élève_by_id(self: object, élève_id: int) -> Elève:
pass
La implementación de la capa [dao] es la siguiente:
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):
# base de datos = {"motor": motor, "metadatos": metadatos, "tablas": tablas, "sesión": sesión}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
…
- línea 11: la clase [DatabaseDao] implementa la interfaz [InterfaceDatabaseDao];
- líneas 13-16: el constructor de la clase. Recibe como parámetro el diccionario de configuración de la aplicación;
- línea 15: se almacena la configuración [sqlalchemy];
- línea 16: se almacena la sesión [sqlalchemy] a través de la cual se manipulará la base de datos;
- línea 18: el método [init_database] inicializa la base de datos con el diccionario [data];
El diccionario [data] se implementa mediante el siguiente 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
# instanciamos las clases
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
classes = [classe1, classe2]
# las materias
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]
# los alumnos
é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]
# las calificaciones de los alumnos en las diferentes materias
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]
# se agrupa todo el conjunto
data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
# se devuelven los datos
return data
- línea 34: el diccionario que se pasará al método [init_database]. Este diccionario está compuesto por las siguientes claves (línea 32):
- [élèves]: la lista de alumnos;
- [classes]: la lista de clases;
- [matières]: la lista de materias;
- [notes]: la lista de calificaciones de todos los alumnos en todas las materias;
Volvamos al método [init_database]:
def init_database(self, data: dict):
# configuración de la base de datos
database = self.database
engine = database['engine']
metadata = database['metadata']
tables = database['tables']
try:
# eliminación de las tablas existentes
# checkfirst=True: primero verifica que la tabla exista
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)
# recreación de las tablas a partir de la asignación
metadata.create_all(engine)
# llenado de las tablas
session = self.session
# clases
classes = data["classes"]
for classe in classes:
session.add(classe)
# materias
matières = data["matières"]
for matière in matières:
session.add(matière)
# alumnos
élèves = data["élèves"]
for élève in élèves:
session.add(élève)
# calificaciones
notes = data["notes"]
for note in notes:
session.add(note)
# confirmación
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# cancelación de la sesión
if session:
session.rollback()
# se eleva la excepción
raise MyException(23, f"{erreur}")
- líneas 3-6: se recupera información de la configuración de la base de datos;
- líneas 9-14: hemos visto que la configuración [sqlalchemy] había asignado cuatro entidades a cuatro tablas [élèves, matières, classes, notes]. Comenzamos por eliminar estas tablas si existen;
- líneas 16-17: recreamos las cuatro tablas que acabamos de eliminar;
- líneas 22-25: agregamos todas las clases a la sesión;
- líneas 27-30: se agregan todas las materias a la sesión;
- líneas 32-35: se agregan todos los alumnos a la sesión;
- líneas 37-40: se agregan todas las calificaciones a la sesión;
- Para realizar estas adiciones, seguimos un orden. Comenzamos con las entidades que no tenían relaciones con otras entidades y terminamos con aquellas que sí las tenían. De esta manera, cuando agregamos a los alumnos a la sesión, las clases a las que estos hacen referencia ya se encuentran en la sesión;
- línea 43: se valida la sesión [sqlalchemy]. Tras esta operación, se tiene la certeza de que todos los datos de la sesión se han sincronizado con la base de datos. En otras palabras, han llegado a las tablas. Esto ha sido posible gracias a las asignaciones que se realizaron en la configuración de [sqlalchemy]. [sqlalchemy] sabe cómo debe almacenarse cada entidad en las tablas. [sqlalchemy] también generó las claves externas que pueden tener las tablas;
- líneas 44-49: si surge un problema, se cancela la sesión de [sqlalchemy] y, en la línea 49, se lanza una excepción;
19.6.6. Inicialización de la base de datos

El script [main_init_database] inicializa la base de datos con el contenido del script [data.py]. Su código es el siguiente:
# se espera un parámetro 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()
# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})
# El syspath está configurado; se pueden realizar las importaciones
from MyException import MyException
# se recuperan los datos para insertarlos en la base de datos
import data
data = data.configure()
# se recupera la capa [dao]
dao = config["dao"]
# ----------- principal
try:
# creación e inicialización de las tablas de la base de datos
dao.init_database(data)
except MyException as ex:
# se muestra el error
print(f"L'erreur suivante s'est produite : {ex}")
finally:
# liberación de los recursos utilizados por la aplicación
import shutdown
shutdown.execute(config)
# fin
print("Travail terminé...")
- líneas 1-11: el script espera un parámetro [mysql] o [pgres], dependiendo de si se desea inicializar una base MySQL o PostgreSQL;
- líneas 13-15: la aplicación está configurada para el parámetro SGBD;
- líneas 20-22: se recuperan los datos que se van a ingresar a la base;
- línea 25: la capa [dao] ya se ha instanciado y está disponible en la configuración de la aplicación;
- línea 30: se inicializa la base de datos;
- líneas 34-37: haya o no un error, se liberan los recursos de la aplicación mediante el módulo [shutdown];
El módulo [shutdown.py] es el siguiente:
def execute(config: dict):
# se liberan los recursos utilizados por la aplicación
sqlalchemy_session = config['database']['session']
if sqlalchemy_session:
sqlalchemy_session.close()
La función [shutdown.execute] cierra la sesión [sqlalchemy] utilizada para inicializar la base de datos.
Creamos una primera configuración de ejecución (véase |configuración de ejecución|) para ejecutar [main_init_database] junto con SGBD y MySQL:

Los resultados de la ejecución de esta configuración son los siguientes en phpMyAdmin:



Para SGBD y [PostgreSQL], utilizamos la siguiente configuración de ejecución:

Al ejecutarlo, los resultados en [pgAdmin] son los siguientes:



Cabe destacar la facilidad con la que se pudo cambiar a SGBD.
19.6.7. La capa [dao] – 2
Volvemos a la clase [DatabaseDao], que implementa la capa [dao]. Hasta ahora solo hemos mostrado la implementación del método [init_database]. A continuación, mostramos la implementación de los demás métodos:
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):
# base de datos = {"motor": motor, "metadatos": metadatos, "tablas": tablas, "sesión": sesión}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
# lista de todas las clases
def get_classes(self: object) -> list:
# consulta
return self.session.query(Classe).all()
# lista de todos los alumnos
def get_élèves(self: object) -> list:
# consulta
return self.session.query(Elève).all()
# lista de todas las materias
def get_matières(self: object) -> list:
# consulta
return self.session.query(Matière).all()
# la lista de calificaciones de todos los alumnos
def get_notes(self: object) -> list:
# consulta
return self.session.query(Note).all()
# la lista de calificaciones de un alumno en particular
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
# se busca al alumno; si no existe, se lanza una excepción
# se permite que se recupere
élève = self.get_élève_by_id(élève_id)
# se recuperan sus calificaciones (carga diferida)
notes = élève.notes
# se devuelve un diccionario
return {"élève": élève, "notes": notes}
# un estudiante identificado por su número
def get_élève_by_id(self, élève_id: int) -> Elève:
# se busca al alumno
élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
# ¿Lo encontramos?
if élèves:
return élèves[0]
else:
raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")
# un alumno identificado por su nombre
def get_élève_by_name(self, élève_name: str) -> Elève:
# Se busca al alumno
élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
# ¿Lo hemos encontrado?
if élèves:
return élèves[0]
else:
raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")
# una clase identificada por su número
def get_classe_by_id(self, classe_id: int) -> Classe:
# Se busca la clase
classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
# ¿Ya la encontramos?
if classes:
return classes[0]
else:
raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")
# ¿Se ha encontrado una clase identificada por su nombre?
def get_classe_by_name(self, classe_name: str) -> Classe:
# se busca la clase
classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
#, ¿la hemos encontrado?
if classes:
return classes[0]
else:
raise MyException(14, f"La classe de nom {classe_name} n'existe pas")
# una asignatura identificada por su número
def get_matière_by_id(self, matière_id: int) -> Matière:
# se busca la materia
matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
# ¿La hemos encontrado?
if matières:
return matières[0]
else:
raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")
# ¿Se ha encontrado un material identificado por su nombre?
def get_matière_by_name(self, matière_name: str) -> Matière:
# se busca la materia
matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
# ¿La hemos encontrado?
if matières:
return matières[0]
else:
raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
- líneas 21-24: el método [get_classes] debe devolver la lista de clases de la escuela. En la línea 20, utilizamos una consulta que ya hemos visto;
- líneas 26-39: otros tres métodos similares para obtener las listas de alumnos, materias y calificaciones;
- líneas 51-59: el método [get_élève_by_id] debe devolver un alumno identificado por su número. Lanza una excepción si este no existe;
- línea 54: se utiliza una consulta filtrada. Se obtiene una lista vacía o con un elemento;
- línea 57: si la lista recuperada no está vacía, se devuelve el primer elemento de la lista;
- de lo contrario, en la línea 59, se lanza una excepción;
- líneas 41-49: el método [get_notes_for_élève_by_id] debe devolver las calificaciones de un alumno identificado por su número:
- línea 45: se utiliza el método [get_élève_by_id] para obtener la entidad Alumno del alumno;
- línea 47: se utiliza la propiedad [Elève.notes] creada por la asignación entre la entidad [Note] y la tabla [notes] (véase el párrafo |configuración de SQLAlchemy|) y que representa las calificaciones del alumno;
- línea 49: se devuelve un diccionario;
- líneas 61-109: una serie de métodos similares que permiten:
- buscar a un alumno por su nombre, líneas 61-69;
- buscar una clase, líneas 71-89;
- buscar una materia, líneas 91-109;
19.6.8. El script [main_joined_queries]

El script [main_joined_queries] se llama así porque tiene como objetivo poner de relieve las consultas realizadas implícitamente por [sqlalchemy] para recuperar información perteneciente a varias tablas. Estas consultas, ocultas para el programador, se realizan cada vez que una propiedad de una entidad se ha asociado a la función [relationship] en la asignación de la entidad. Por ejemplo:
# mapeo
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'valor': notes_table.c.valeur,
'alumno': relación(Alumno, backref="notas", lazy="seleccionar"),
'asignatura': relación(Asignatura, backref="notas", lazy="select")
})
Arriba, la asignación entre la entidad [Note] y la tabla [notes]:
- línea 5: cuando se solicita por primera vez la propiedad [élève] de una entidad [Note], se buscará en la tabla [élèves] mediante una consulta SQL. Mientras no se haya solicitado esta propiedad, permanece indefinida (carga diferida). Una vez obtenida, su valor permanece en la memoria de la tabla ORM. Cuando se haga referencia a ella por segunda vez, el ORM entregará inmediatamente su valor sin necesidad de realizar una nueva consulta SQL. Todo esto es transparente para el desarrollador;
- lo mismo ocurre con la propiedad inversa [Elève.notes] (backref), línea 5;
- lo mismo ocurre con la propiedad [Note.matière] y su propiedad inversa [Matière.notes] (backref), línea 6;
El script [main_joined_queries] es el siguiente:
# se espera un parámetro 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()
# se configura la aplicación
import config
config = config.configure({"sgbd": sgbd})
# El syspath está configurado; ya se pueden realizar las importaciones
from MyException import MyException
# la capa [dao]
dao = config["dao"]
try:
# alumno por id
print("élève id=11 -----------")
élève = dao.get_élève_by_id(11)
print(f"élève={élève}")
# la clase del alumno (carga diferida)
classe = élève.classe
print(f"classe de l'élève : {classe}")
# los alumnos de la misma clase (carga diferida)
print("élèves dans la même classe :")
for élève in classe.élèves:
print(f"élève={élève}")
# un alumno por su nombre
print("élève nom='nom2' -----------")
print(f"élève={dao.get_élève_by_name('nom2')}")
# su clase (carga diferida)
print(f"classe de l'élève : {élève.classe}")
# calificaciones de un alumno
print("notes de l'élève id=11 -----------")
# primero el alumno
élève = dao.get_élève_by_id(11)
# luego sus calificaciones (carga diferida)
for note in élève.notes:
# la nota
print(f"note={note}, "
# la materia de la nota (carga diferida)
f"matière={note.matière}")
# los alumnos de una clase
print("élèves de la classe nom='classe1' -----------")
# primero la clase
classe = dao.get_classe_by_name('classe1')
# luego los alumnos (carga diferida)
for élève in classe.élèves:
print(élève)
# lo mismo para [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)
# las calificaciones en una materia
print("matière de nom='matière1' -----------")
# primero la materia
matière = dao.get_matière_by_name('matière1')
print(f"matière={matière}")
# luego las calificaciones en esa materia (carga diferida)
print("Notes dans la matière : ")
for note in matière.notes:
print(note)
# lo mismo para la asignatura 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:
# se muestra el error
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# se muestra el error
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# se liberan los recursos
import shutdown
shutdown.execute(config)
Los comentarios son suficientes para entender el código.
Creamos una configuración de ejecución para MySQL:

Los resultados de la ejecución son los siguientes:
Para entender estos resultados, hay que recordar que se han excluido ciertas propiedades del diccionario de entidades (véase |configuración|):
# Configuración de las entidades [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']
Así, cuando escribimos [print(f"élève={élève}")] en la línea 26 del código, la línea 1 anterior nos indica que las propiedades ['_sa_instance_state', 'notes', 'classe'] no se mostrarán. Esto es lo que vemos en la línea 3 de los resultados. Todas las demás propiedades se muestran. Así, siempre en la línea 3, descubrimos una nueva propiedad [classe_id] que inicialmente no existía en la entidad [Elève]. Esta propiedad corresponde directamente a la columna [classe_id] de la tabla [élèves]. Así, [sqlalchemy] ha agregado las siguientes propiedades a la entidad [Elève]: [classe_id, _sa_instance_state, notes]. Es importante tenerlo en cuenta, sobre todo porque estas propiedades no deben existir ya en la entidad mapeada.
Las propiedades excluidas del diccionario de entidades son importantes. Si, por ejemplo, no se excluyen las propiedades [notes, élève] de la entidad [Elève], entonces la operación [print(f"élève={élève}")] las mostrará y, por lo tanto, como se acaba de explicar, provocará consultas implícitas SQL (carga diferida) para recuperar los valores de esas propiedades. Si, como en este caso, se muestra una lista de alumnos, las operaciones implícitas SQL se realizan para cada alumno. Esto puede ser, por un lado, innecesario y, por otro, seguramente costoso en tiempo de ejecución.
Para ejecutar el script con una base PostgreSQL, se crea la siguiente configuración de ejecución:

La ejecución arroja los mismos resultados que con MySQL.
19.6.9. El script [main_stats_for_élève]

El script [main_stats_for_élève] es el que ya se utilizó en la aplicación |troiscouches v01]. En ese entonces se llamaba [main]. Se trata de una aplicación de consola que permite obtener ciertos indicadores sobre las calificaciones de un alumno: [moyenne pondérée, min, max, liste]. Se integra en la siguiente arquitectura:

En esta arquitectura por capas, solo se modificó la capa [dao] entre la aplicación |troiscouches v01| y esta. Dado que la nueva capa [dao] respeta la interfaz [InterfaceDao] de la antigua capa [dao], no es necesario modificar las capas [ui, métier]. Por lo tanto, se pueden seguir utilizando las definidas en la aplicación |troiscouches v01|.
El script [main_stats_for_élève] implementa la capa [main] del esquema anterior de la siguiente manera:
# se espera un parámetro 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()
# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})
# El syspath está configurado; ya se pueden realizar las importaciones
from MyException import MyException
# la capa [ui]
ui = config["ui"]
try:
# ejecución de la capa [ui]
ui.run()
except MyException as ex1:
# se muestra el error
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# se muestra el error
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
# se liberan los recursos
import shutdown
shutdown.execute(config)
- línea 20: se obtiene una referencia a la capa [ui] en la configuración de la aplicación;
- línea 24: se inicia el diálogo con el usuario mediante el único método de la capa [ui];
Una configuración de ejecución para PostgreSQL sería la siguiente:

A continuación se muestra un ejemplo de ejecución con esta configuración:
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