Skip to content

19. Uso de ORM SQLALCHEMY

El capítulo anterior ha mostrado que, en algunos casos, se puede escribir código independiente de SGBD utilizado con la siguiente arquitectura:

Image

En este capítulo utilizaremos el ORM (Object Relational Mapper) [sqlalchemy] para acceder a los SGBD de manera uniforme, independientemente del SGBD utilizado. 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 queda ahora 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 es insensible a un cambio de conector (y, por lo tanto, del SGBD);

El árbol de los scripts estudiados será el siguiente:

Image

19.1. Instalación del ORM [sqlalchemy]

El ORM [sqlalchemy] se presenta en forma de 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: conceptos básicos

Image

  • en [1], los scripts que se van a estudiar. Estos scripts utilizarán las clases de [2]: BaseEntity, MyException, Personne, Utils;

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, Utilidades
        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 genera el config
    return {}

Comentarios

  • línea 8: se indica en el Python Path la carpeta que contiene las clases [BaseEntity, MyException, Personne, Utils];
  • líneas 12-13: se establece el Python Path de la aplicación;
  • líneas 16-17: quizá 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 [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,
    'prénom': personnes_table.c.prenom,
    'nom': personnes_table.c.nom,
    'âge': personnes_table.c.age
})

# persona1 no se ha modificado
print(f"personne1={personne1.__dict__}")
# la clase Persona sí se ha modificado: se ha «enriquecido»
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 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 principal de la tabla [personnes];
        • línea 18: [nullable=False] para indicar que una columna debe tener necesariamente un valor cuando se inserta una línea en la tabla;
    • línea 21: por último, la clase [UniqueConstraint] permite describir una restricción de unicidad. Aquí se indica que las columnas (nombre, apellidos) 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 introduce 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 crea una correspondencia entre la clase [Personne] y la tabla [personnes]. Se trata, básicamente, 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 va a asociar. Esta es el objeto [Table] creado en la línea 16;
    • línea 28: el tercer parámetro es aquí 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 en profundidad:

  • (continuación)
    • aparece una nueva propiedad [_sa_instance_state]. Se ve 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-33 ha modificado 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 ha escrito 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] interactuando con [sqlalchemy]. Para comprender lo que sigue, hay que recordar la arquitectura utilizada aquí:

Image

Si [Database1] es la base [dbpersonnes], vemos que la conexión entre el script y esta base pasa por dos entidades:

  • el conector Python en SGBD MySQL;
  • el SGBD MySQL;

El script [main] se comunicará con el ORM, que a su vez se comunicará con el conector Python. El ORM se comunica con este conector mediante las herramientas descritas en los apartados |MySQL| y |PostgreSQL|, en particular emitiendo ó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,
    'prénom': personnes_table.c.prenom,
    'nom': personnes_table.c.nom,
    'âge': 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 del mapeo
    metadata.create_all(engine)

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

    # una solicitud
    personnes = session.query(Personne).all()

    # visualización
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # otras dos inserciones, la segunda de las cuales falla debido a la unicidad (nombre, apellidos)
    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 toda 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 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 se ha dicho, una sesión puede equipararse a una transacción;
  • líneas 45-46: el método [Session.execute] permite ejecutar una orden SQL. No es algo habitual, ya que se ha dicho que ORM permitía evitar el lenguaje SQL;
  • líneas 48-49: el método [metadata.create_all] permite crear todas las tablas utilizando 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 SGBD. De hecho, la orden SQL [create] puede ser muy diferente de una SGBD a otra debido a los tipos asignados a las columnas. No se ha producido una estandarización de los tipos de datos. Así, el orden varía de un caso a otro. Aquí, gracias a esto:
    • describimos de forma única la tabla que deseamos;
    • [sqlalchemy] se encarga de generar el [create] adecuado para el SGBD que tiene delante;
  • línea 52: se añade un objeto [Personne] a la sesión. Esto no lo añade automáticamente a la base de datos. De hecho, un ORM sigue sus propias reglas para sincronizarse con la base de datos. Siempre intentará optimizar el número de consultas que realiza. Veamos un ejemplo. El script añade (add) dos personas (persona1, persona2) a la sesión y, a continuación, realiza una consulta: quiere ver todas las personas presentes 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 introducirla en la base de datos;
    • lo mismo ocurre con [personne2];
    • a continuación viene la consulta de tipo [select]. Entonces hay que recuperar todas las filas de la tabla [personnes]. [sqlalchemy] introducirá 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 de 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__], que devuelve la cadena jSON del objeto llamante. 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 cuelga». Esto es lo que se ha hecho en el script [config];
  • líneas 63-65: se añaden otras dos personas que tienen el mismo nombre y apellidos. Sin embargo, existe una restricción de unicidad en la unión de estas dos columnas. Por lo tanto, debería producirse un error. Esto es lo que queremos comprobar;
  • líneas 67-68: volvemos a solicitar la lista de todas las personas de la base;
  • líneas 70-73: y las mostramos;
  • líneas 75-76: la sesión se valida («commit»). Como su nombre indica, la transacción subyacente se va a validar;
  • veremos en 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 gestionar la excepción;
  • línea 78: se produce la excepción [InterfaceError] si [sqlalchemy] no consigue 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 tras la primera inserción;
  • línea 5: la excepción [IntegrityError] que se produjo al añadir dos personas con el mismo nombre y apellidos;
  • líneas 6-7: cabe destacar la orden SQL que ha fallado. Se trata de una orden INSERT configurada: [sqlalchemy] insertó a las dos personas con un único INSERT. Aquí vemos que ha intentado optimizar las órdenes SQL emitidas;

Ahora veamos, con phpMyAdmin, el contenido de la tabla [personnes]:

Image

En [6] vemos que la tabla está vacía. Ni siquiera aparece la primera persona que el script había incluido en la sesión. Esto se debe a que dicha sesión se desarrollaba en una transacción y esta se ha revertido en la cláusula [except] del script [main].

Procedamos ahora a realizar la siguiente modificación en [main]:


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

Tras añadir 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:

Image

En [6] vemos que se ha conservado la primera inserción. Esto se debe a que se realizó dentro de una transacción 1 y a que el error posterior se produjo dentro de una transacción 2.

19.3. Scripts 02: las asignaciones de [sqlalchemy]

Image

Los scripts 02 son una variante de los scripts 01. Intentamos realizar el máximo de configuraciones en [config.py]. Ahora configuramos en él 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,
        'prénom': personnes_table.c.prenom,
        'nom': personnes_table.c.nom,
        'âge': personnes_table.c.age
    })

    # la fábrica de sesiones
    Session = sessionmaker()
    Session.configure(bind=engine)

    # se introduce esta información en la config
    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 genera el config
    return config

Comentarios

  • líneas 2-12: configuración de Python Path;
  • líneas 14-45: se configura el entorno [sqlalchemy];
  • líneas 47-52: el entorno [sqlalchemy] se incluye en el 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()

# se configura la ruta del sistema - 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"]()

    # eliminación de 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, la segunda de las cuales 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] ha quedado así:

Image

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

Image

  • en [6], los tipos utilizados para las diferentes columnas;
  • en [7], vemos que la columna [id] tiene el atributo [AUTO_INCREMENT]. Esto significa que, al insertar una fila en la tabla, si dicha fila no tiene ningún valor para la columna [id], este será generado por MySQL de forma incremental: 1, 2, 3, … Esta propiedad nos evita tener que preocuparnos por el valor de la clave primaria cuando realizamos 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]

Image

El archivo de configuración [config] es el mismo que en el ejemplo anterior. En el script [main] se realizan las operaciones clásicas [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)


# mano ---------------------------
session = None
try:
    # una sesión
    session = config["Session"]()

    # eliminación de la tabla [personnes]
    # checkfirst=True: comprueba primero si la tabla existe
    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 por orden alfabético de los apellidos y, en caso de apellidos iguales, por orden alfabético de los nombres
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # visualización
    affiche_personnes(personnes)

    # lista de personas con una edad comprendida en el intervalo [20,40] en orden descendente de 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] exige 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 añaden tres personas a la sesión. Cabe recordar que no se insertan necesariamente 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 las inserciones de las tres personas aún no se habían realizado, ahora se hacen 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;
  • líneas 70-71: otro ejemplo de consulta filtrada. La función [func.lower(param)] devuelve [param] en 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:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/03/main.py
----------------
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0
  • líneas 4-6: el contenido de la sesión;
  • líneas 8-10: el contenido de la sesión en orden descendente por nombres;
  • 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 final de la ejecución es el siguiente:

Image

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

Image

El archivo [04] es una copia del archivo [03]. Solo se cambia una cosa, la cadena de conexión en el archivo [config]:


    # lien vers une base de données 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]. Es necesario que este esté instalado.

La ejecución del script [main] da los siguientes resultados:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/04/main.py
----------------
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0

Con la herramienta [pgAdmin] (véase el apartado |pgAdmin|), la tabla [personnes] se encuentra en el siguiente estado:

Image

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

Image

  • En [4-5], se observa que la columna [id] es la clave primaria. También se ve que tiene un valor por defecto [mot clé DEFAULT], lo que hace que, si se inserta una línea sin clave primaria, esta sea generada por el SGBD. Es un funcionamiento habitual: se deja que SGBD genere las claves primarias;

Este version 05 de los scripts [sqlalchemy] muestra claramente lo fácil que es pasar de un SGBD a otro: bastó con cambiar la cadena de conexión en un script de configuración. No ha cambiado nada más. Si comparamos los tipos de las columnas [id, nom, prenom, age] anteriores 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 motivo suficiente para adoptar [sqlalchemy] u otro ORM.

19.6. Scripts 05: ejemplo completo

Image

El ejemplo estudiado es una repetición del analizado en el apartado |troiscouches-v01|. Este 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 colocamos 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:

Image

  • En [1-3] se encuentran las capas [ui, métier, dao] que ya estaban presentes en el ejemplo |troiscouches-v01|. La capa [dao] se comunica ahora con la capa [ORM];
  • las capas [1-5] se implementan mediante código Python;

19.6.2. Las bases de datos

Creamos una base de datos MySQL denominada [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|:

Image

Image

  • en [1], la base [dbecole] sin las tablas [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|:

Image

  • 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 manipuladas 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 buscaremos allí 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 asignatura
        # nombre: nombre de la asignatura
        # coeficiente: coeficiente de la asignatura
        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):
        # 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 nota
        # valor: la propia nota
        # alumno: alumno (de tipo Alumno) al que se refiere la nota
        # asignatura: asignatura (de tipo Asignatura) a la que se refiere la nota
        # el objeto «Nota» es, por tanto, la nota de un alumno en una asignatura
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # getters
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # getters
    @valeur.setter
    def valeur(self: object, valeur: float):
        # 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

Image

La configuración se ha dividido en varios archivos:

  • la configuración general en [config.py]: establece el Python Path de la aplicación e instancia las capas de la arquitectura;
  • la configuración de [sqlalchemy] en [config_database]: realiza las asignaciones de clases/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 el Python Path 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 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
        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",
        # carpetas del presente proyecto
        script_dir,
        f"{script_dir}/../services",
    ]

    # actualización de syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # etapa 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 genera el config
    return config
  • líneas 4-27: construcción del Python Path de la aplicación;
  • líneas 29-32: configuración [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 utilizadas
    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,
        'nom': 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')),
              )
    # asignación
    mapper(Elève, tables['élèves'], properties={
        'id': élèves_table.c.id,
        'nom': élèves_table.c.nom,
        'prénom': élèves_table.c.prénom,
        'classe': relationship(Classe, backref="élèves", lazy="select")
    })

    # la tabla de contenidos
    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,
        'nom': matières_table.c.nom,
        "coefficient": matières_table.c.coefficient
    })

    # la tabla de notas
    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 es la de una asignatura
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # asignación
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valeur': notes_table.c.valeur,
        'élève': relationship(Elève, backref="notes", lazy="select"),
        'matière': relationship(Matière, backref="notes", lazy="select")
    })

    # configuración de 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 guarda cierta información en el diccionario de configuración
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # se devuelve el config
    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 [dbecole] y las entidades [Classes, Elève, Matière, Note]. En la tabla, los datos de la entidad están encapsulados en una línea. En el código 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, el Python Path ya habrá sido establecido 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 principal 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, a continuación, 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 matrícula del alumno, 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 contener 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 clave externa. [élèves.classe_id] es una clave externa (ForeignKey) en 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 por 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] mantiene una relación de clave externa. Esta debe materializarse 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 añadirá 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, basta con elegir aquí otro nombre para [backref]. El desarrollador no tiene que gestionar esta propiedad. [sqlalchemy] se encargará de ello. Solo debe saber que existe, que ha sido añadida por [sqlalchemy], y que puede utilizarla en su código;
      • [lazy=’select’]: esto significa que ORM no debe intentar asignar inmediatamente un valor 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 [e] concreto y hace referencia a su clase [e.classe]. Esta referencia obligará entonces a [sqlalchemy] a realizar una consulta en la base de datos para recuperar la clase del alumno, de forma 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], recupera la información de [id, nom, prénom, classe_id]. A partir de ahí, debe construir un objeto Alumno(id, apellido, nombre, clase). Para las propiedades [id, nom, prénom] esto no plantea dificultades. En cuanto a la propiedad [classe], es más complicado. Su valor es una referencia a un objeto de tipo [Classe]. Sin embargo, ORM solo contiene información de [é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 en la tabla [classes] la fila con el n.º id=[élèves.classe_id] (que necesariamente existe) y que, a partir de esta fila, cree el objeto [Classe] esperado por 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 es la nota de un alumno concreto;
    • línea 81, la columna [notes.matière_id] toma sus valores de la columna [matières.id]. Esta clave externa materializa el hecho de que una nota es una nota de una asignatura concreta;
  • 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 de tipo [Elève]. La tabla ORM solo contiene, en la línea 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 que se dispone de la nota. Por otra parte, [relationship(Elève, backref="notes", …)] creará la nueva propiedad [Elève.notes], que será la lista de notas 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 ORM solo contiene, en la línea 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 dispone de 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 deben excluirse del diccionario de propiedades de la entidad (BaseEntity.asdict). Hemos visto que [sqlalchemy] añadía la propiedad [_sa_instance_state] a todas las entidades mapeadas. No la queremos en el diccionario de propiedades. Por otra parte, hemos visto que los mapeos anteriores habían añadido nuevas propiedades a las entidades:
    • [Elève.notes]: todas las notas del alumno;
    • [Classe.élèves]: todos los alumnos de la clase;
    • [Matière.notes]: todas las notas de la asignatura;

En general, no queremos que estas propiedades se añadan al estado de la entidad. De hecho, calcular su valor tiene un coste SQL y este valor suele ser innecesario. Así, si recuperamos al alumno llamado «X»:

  • (continuación)
    • 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á obligada a realizar consultas 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 introducen en el diccionario global de la 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 colocan las capas en la config
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # se convierte en config
    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

Image

Hay que tener en cuenta que la capa [dao] [3] se comunica con laORM [sqlalchemy] [4] configurada tal y como se ha descrito en el párrafo anterior. De las tres capas [ui, métier, dao] de la aplicación |troiscouches v01|, solo debe reescribirse la capa [dao]. Las capas [ui, métier] se conservan.

La implementación de la capa [dao] se ha colocado en la carpeta [services]:

Image

[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 añade 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 asignaturas
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

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

    # lista de notas 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 va a 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 asignaturas
    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 notas de los alumnos en las diferentes asignaturas
    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 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 asignaturas;
    • [notes]: la lista de notas de todos los alumnos en todas las asignaturas;

Volvamos al método [init_database]:


def init_database(self, data: dict):
        # config 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: comprueba primero que la tabla existe
            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)

            # rellenado de las tablas
            session = self.session

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

            # asignaturas
            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)

            # notas
            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 reenvía 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 mapeado cuatro entidades en cuatro tablas [élèves, matières, classes, notes]. Empezamos por eliminar estas tablas si existen;
  • líneas 16-17: se vuelven a crear las cuatro tablas que acabamos de eliminar;
  • líneas 22-25: se añaden todas las clases a la sesión;
  • líneas 27-30: se añaden todas las asignaturas a la sesión;
  • líneas 32-35: se añaden todos los alumnos a la sesión;
  • líneas 37-40: se añaden todas las notas a la sesión;
  • para realizar estas adiciones, hemos seguido un orden. Hemos empezado por las entidades que no tenían relaciones con otras entidades para terminar con las que sí las tenían. Así, cuando añadimos a los alumnos a la sesión, las clases a las que estos hacen referencia ya están en la sesión;
  • línea 43: se ha validado la sesión [sqlalchemy]. Tras esta operación, se garantiza que todos los datos de la sesión se han sincronizado con la base de datos. En otras palabras, se han incorporado a las tablas. Esto ha sido posible gracias a las asignaciones realizadas en la configuración de [sqlalchemy]. [sqlalchemy] sabe cómo debe almacenarse cada entidad en las tablas. [sqlalchemy] también ha generado las claves externas que pueden tener las tablas;
  • líneas 44-49: si se produce un problema, se cancela la sesión [sqlalchemy] y, en la línea 49, se lanza una excepción;

19.6.6. Inicialización de la base de datos

Image

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 que se van a introducir en la base de datos
import data
data = data.configure()

# se recupera la capa [dao]
dao = config["dao"]

# ----------- main
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 SGBD pasado como parámetro;
  • líneas 20-22: se recuperan los datos que se van a introducir en la base;
  • línea 25: la capa [dao] ya se ha instanciado y es accesible en la configuración de la aplicación;
  • línea 30: se inicializa la base de datos;
  • líneas 34-37: haya error o no, 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] con SGBD y MySQL:

Image

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

Image

Image

Image

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

Image

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

Image

Image

Image

Cabe destacar la facilidad con la que se ha podido cambiar de 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 asignaturas
    def get_matières(self: object) -> list:
        # consulta
        return self.session.query(Matière).all()

    # la lista de notas de todos los alumnos
    def get_notes(self: object) -> list:
        # consulta
        return self.session.query(Note).all()

    # lista de notas de un alumno concreto
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # se busca al alumno; se lanza una excepción si no existe
        # se deja que se recupere
        élève = self.get_élève_by_id(élève_id)
        # se recuperan sus notas (carga diferida)
        notes = élève.notes
        # se devuelve un diccionario
        return {"élève": élève, "notes": notes}

    # un alumno identificado por su n.º
    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 hemos encontrado?
        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.º
    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()
        # ¿La hemos encontrado?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # 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.º
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # se busca la asignatura
        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")

    # un material identificado por su nombre
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # se busca el material
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # ¿se ha 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 del colegio. 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, asignaturas y notas;
  • líneas 51-59: el método [get_élève_by_id] debe devolver un alumno identificado por su n.º. 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 notas de un alumno identificado por su n.º:
    • 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 apartado |configuración de SQLAlchemy|) y que representa las notas del alumno;
    • línea 49: se devuelve un diccionario;
  • líneas 61-109: una serie de métodos análogos que permiten:
    • buscar a un alumno por su nombre, líneas 61-69;
    • buscar una clase, líneas 71-89;
    • buscar una asignatura, líneas 91-109;

19.6.8. El script [main_joined_queries]

Image

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


    # mapping
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valeur': notes_table.c.valeur,
        'élève': relationship(Elève, backref="notes", lazy="select"),
        'matière': relationship(Matière, backref="notes", 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, permanecerá indefinida (carga diferida). Una vez obtenida, su valor permanecerá en la memoria de ORM. Cuando se haga referencia a ella por segunda vez, ORM proporcionará inmediatamente su valor sin pasar por 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; 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}")

    # notas 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 notas (carga diferida)
    for note in élève.notes:
        # la nota
        print(f"note={note}, "
              # el tema 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 notas de una asignatura
    print("matière de nom='matière1' -----------")
    # primero la asignatura
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # luego las notas de esa asignatura (carga diferida)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # lo mismo para asignatura2
    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 bastan para comprender el código.

Creamos una configuración de ejecución para MySQL:

Image

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/05/main/main_joined_queries.py mysql
élève id=11 -----------
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
classe de l'élève : {"nom": "classe1", "id": 1}
élèves dans la même classe :
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élève nom='nom2' -----------
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
classe de l'élève : {"nom": "classe1", "id": 1}
notes de l'élève id=11 -----------
note={"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}, matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}, matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
élèves de la classe nom='classe1' -----------
{"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
{"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élèves de la classe de nom 'classe2' -----------
{"classe_id": 2, "nom": "nom3", "prénom": "prénom3", "id": 32}
{"classe_id": 2, "nom": "nom4", "prénom": "prénom4", "id": 42}
matière de nom='matière1' -----------
matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
Notes dans la matière : 
{"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}
{"matière_id": 1, "valeur": 12.0, "élève_id": 21, "id": 2}
{"matière_id": 1, "valeur": 14.0, "élève_id": 32, "id": 3}
{"matière_id": 1, "valeur": 16.0, "élève_id": 42, "id": 4}
matière de nom='matière2' -----------
matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
Notes dans la matière : 
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}
note={"matière_id": 2, "valeur": 8.0, "élève_id": 21, "id": 6}
note={"matière_id": 2, "valeur": 10.0, "élève_id": 32, "id": 7}
note={"matière_id": 2, "valeur": 12.0, "élève_id": 42, "id": 8}

Process finished with exit code 0

Para entender estos resultados, hay que recordar que se han excluido ciertas propiedades del diccionario de entidades (véase |configuración|):


    # configuration des entités [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 se escribe [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. Se muestran todas las demás propiedades. 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 añadido 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], la operación [print(f"élève={élève}")] las mostrará y, por lo tanto, como se acaba de explicar, provocará consultas SQL implícitas (carga diferida) para recuperar los valores de estas propiedades. Si, como en este caso, se muestra una lista de alumnos, las operaciones implícitas SQL se realizan para cada alumno. Esto puede resultar, 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:

Image

La ejecución da 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 utilizaba en la aplicación |troiscouches v01]. Entonces se llamaba [main]. Se trata de una aplicación de consola que permite obtener ciertos indicadores sobre las notas de un alumno: [moyenne pondérée, min, max, liste]. Se inserta en la siguiente arquitectura:

Image

En esta arquitectura por capas, solo se ha modificado 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})

# se ha configurado la ruta del sistema (syspath); 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 recupera 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:

Image

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