Skip to content

19. Utilização do ORM SQLALCHEMY

O capítulo anterior demonstrou que, em certos casos, é possível escrever código independente do SGBD, utilizado com a seguinte arquitetura:

Image

Neste capítulo, vamos utilizar o ORM (Mapeador Objeto-Relacional) [sqlalchemy] para aceder aos SGBD de forma uniforme, independentemente do SGBD utilizado. Um ORM permite duas coisas:

  • permite que um script comunique com o SGBD sem emitir comandos SQL;
  • oculta ao script as particularidades de cada SGBD;

A arquitetura passa a ser a seguinte:

O script está agora separado dos conectores pelo ORM. Interage com o ORM através de classes e métodos. Não executa código SQL. É o ORM que o faz com os conectores aos quais está ligado. Este oculta ao script as particularidades desses conectores. Assim, o código do script é insensível a uma mudança de conector (ou seja, do SGBD);

A estrutura hierárquica dos scripts analisados será a seguinte:

Image

19.1. Instalação do ORM [sqlalchemy]

O ORM [sqlalchemy] vem na forma de um pacote Python que deve ser instalado num terminal 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: o básico

Image

  • em [1], os scripts que serão analisados. Estes scripts irão utilizar as classes de [2]: BaseEntity, MyException, Pessoa, Utilitários;

19.2.1. Configuração

O ficheiro [config] configura a aplicação da seguinte forma:


def configure():
    # root_dir
    # caminho absoluto de referência dos caminhos relativos da configuração
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # caminhos absolutos das dependências
    absolute_dependencies = [
        # BaseEntity, MyException, Pessoa, Utilitários
        f"{root_dir}/classes/02/entities",
    ]

    # define-se o syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # configuração das classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # aplicamos a configuração
    return {}

Comentários

  • linha 8: adiciona-se ao Python Path a pasta que contém as classes [BaseEntity, MyException, Personne, Utils];
  • linhas 12-13: define-se o Python Path da aplicação;
  • linhas 16-17: talvez se lembre que a classe |BaseEntity| tem um atributo de classe denominado [excluded_keys]. Este atributo é uma lista na qual se colocam as propriedades da classe que não se quer que apareçam no dicionário da mesma (função asdict). Aqui, exclui-se a propriedade [_sa_instance_state] do estado da classe [Personne]. Veremos em breve porquê;

19.2.2. Script [démo]

O script [démo] mostra uma primeira utilização do ORM [sqlalchemy]:


# recupera-se a configuração da aplicação
import config

config = config.configure()

# importações
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper

from Personne import Personne

# metadados
metadata = MetaData()

# a tabela
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )
# a classe Pessoa antes do mapeamento
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")

# o mapeamento
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'nome próprio: personnes_table.c.prenom,
    'apelido: personnes_table.c.nom,
    'idade': personnes_table.c.age
})

# a pessoa 1 não foi alterada
print(f"personne1={personne1.__dict__}")
# a classe Pessoa foi alterada — foi «enriquecida»
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

Comentários

  • linhas 1-4: configura-se a aplicação;
  • linhas 6-10: importam-se os módulos necessários para o script;
  • linha 13: [MetaData] é uma classe de [sqlalchemy];
  • linhas 15-22: [Table] é uma subclasse de [sqlalchemy]. Permite descrever uma tabela de uma base de dados. Aqui, vamos descrever a tabela [personnes] da base de dados MySQL [dbpersonnes] analisada no capítulo |MySQL|;
    • linha 16: o primeiro parâmetro [personnes] é o nome da tabela descrita;
    • linha 16: o segundo parâmetro [metadata] é a instância [MetaData] criada na linha 13;
    • linhas 17-22: cada um dos parâmetros seguintes descreve uma coluna da tabela com uma sintaxe própria de [sqlalchemy], mas mais próxima da sintaxe de SQL;
    • cada coluna é descrita por uma instância da classe [Column] de [sqlalchemy];
      • o primeiro parâmetro é o nome da coluna;
      • o segundo parâmetro é o seu tipo;
      • os parâmetros seguintes são parâmetros nomeados:
        • linha 17: [primary_key=True] para indicar que a coluna [id] é a chave primária da tabela [personnes];
        • linha 18: [nullable=False] para indicar que uma coluna deve obrigatoriamente ter um valor quando uma linha é inserida na tabela;
    • linha 21: por fim, a classe [UniqueConstraint] permite descrever uma restrição de unicidade. Aqui, indica-se que as colunas (apelido, nome próprio) devem ser únicas na tabela. A propriedade denominada [name] permite atribuir um nome a esta restrição. Neste caso, é necessário distinguir dois casos:
      • estamos a descrever uma tabela existente. Nesse caso, é necessário procurar o nome da restrição nas propriedades da tabela (phpMyAdmin ou pgAdmin);
      • estamos a descrever uma tabela que vamos criar. Nesse caso, introduzimos o nome que quisermos;
  • linhas 23-25: criamos uma pessoa [personne1] e apresentamos o seu dicionário [__dict__]. Aqui teremos:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • linhas 27-33: faz-se um mapeamento, ou seja, cria-se uma correspondência entre a classe [Personne] e a tabela [personnes]. Trata-se essencialmente de uma correspondência [propriétés de la classe  colonnes de la table]. A função [mapper] aceita aqui três parâmetros:
    • linha 28: o primeiro parâmetro é o nome da classe para a qual se efetua o mapeamento;
    • linha 28: o segundo parâmetro é a tabela à qual será associada. Trata-se do objeto [Table] criado na linha 16;
    • linha 28: o terceiro parâmetro é, neste caso, um parâmetro denominado [properties]. Trata-se de um dicionário em que as chaves são as propriedades da classe mapeada e os valores são as colunas da tabela mapeada. Para designar a coluna X da tabela [personnes_table], escreve-se [personnes_table.c.X];
  • linhas 35-36: volta a apresentar a pessoa [personne1] após a realização do mapeamento. Verifica-se que não sofreu alterações:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • linhas 37-39: cria-se uma nova pessoa [personne2] e exibe-se a mesma. Obtém-se então a seguinte exibição:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}

Verifica-se que o dicionário [__dict__] sofreu alterações profundas:

  • (continuação)
    • aparece uma nova propriedade [_sa_instance_state]. Vê-se que se trata de um objeto do ORM [sqlalchemy];
    • as restantes propriedades foram despojadas do seu prefixo, que indicava a que classe pertenciam;

Pode-se, portanto, concluir que a operação de mapeamento das linhas 27-33 alterou a classe [Personne].

Quando se pretender visualizar o estado de um objeto [Personne], normalmente não se pretende a propriedade [_sa_instance_state]. Na verdade, esta existe apenas para o funcionamento interno de [sqlalchemy] e, em geral, não nos interessa. É por isso que escrevemos no script [config]:


    # configuração das classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

19.2.3. O script [main]

O script [main] irá manipular a tabela [personnes] da base de dados MySQL [dbpersonnes], interagindo com a tabela [sqlalchemy]. Para compreender o que se segue, é necessário recordar a arquitetura aqui utilizada:

Image

Se [Database1] for a base [dbpersonnes], verifica-se que a ligação entre o script e esta base passa por duas entidades:

  • o conector Python para o SGBD MySQL;
  • o SGBD MySQL;

O script [main] irá interagir com o ORM, que, por sua vez, irá interagir com o conector Python. O ORM comunica com este conector através das ferramentas descritas nos parágrafos |MySQL| e |PostgreSQL|, nomeadamente emitindo comandos SQL. O script [main] não irá utilizar comandos SQL. Vai basear-se na API (Interface de Programação de Aplicações) da ORM, composta por classes e interfaces.

O script [main] é o seguinte:


# configuramos a aplicação
import config

config = config.configure()

# importações
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

# cadeia de ligação a uma base de dados MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

# metadados
metadata = MetaData()

# a tabela
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )

# o mapeamento
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id,
    'nome próprio: personnes_table.c.prenom,
    'apelido: personnes_table.c.nom,
    'idade': personnes_table.c.age
})

# a fábrica de sessões
Session = sessionmaker()
Session.configure(bind=engine)

session = None
try:
    # uma sessão
    session = Session()

    # eliminação da tabela [personnes]
    session.execute("drop table if exists personnes")

    # recriação da tabela a partir do mapeamento
    metadata.create_all(engine)

    # uma inserção
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

    # uma consulta
    personnes = session.query(Personne).all()

    # exibição
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # duas outras inserções, das quais a segunda falha devido à regra de unicidade (nome, apelido)
    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}))

    # uma consulta
    personnes = session.query(Personne).all()

    # visualização
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # validação da sessão
    session.commit()

except (InterfaceError, IntegrityError) as erreur:
    # exibição
    print(f"L'erreur suivante s'est produite : {erreur}")
    # cancelamento da última sessão
    if session:
        print("rollback...")
        session.rollback()
finally:
    # libertação dos recursos da sessão
    if session:
        session.close()

Comentários

  • linhas 1-4: a aplicação é configurada;
  • linhas 7-9: importa-se toda uma série de classes e interfaces da biblioteca [sqlalchemy];
  • linha 11: a classe [Personne] é importada;
  • linha 14: a cadeia de ligação à base de dados. Esta especifica:
    • o SGBD utilizado (mysql);
    • o conector Python utilizado (mysql.connector sem o ponto);
    • o utilizador que se liga (admpersonnes);
    • a sua palavra-passe (nobody);
    • a máquina onde se encontra o SGBD (localhost = máquina onde se encontra o script que está a ser executado);
    • o nome da base de dados (dbpersonnes);

Com estas informações, o [sqlalchemy] pode ligar-se à base de dados. Note-se que o conector Python utilizado já deve estar instalado. O [sqlalchemy] não o faz.

  • linhas 19-26: descrição da tabela [personnes];
  • linhas 28-34: mapeamento entre a classe [Personne] e a tabela [personnes];
  • linhas 36-38: a maioria das operações [sqlalchemy] é realizada numa sessão. O conceito de sessão [sqlalchemy] é semelhante ao de transação SQL. As sessões são criadas a partir da classe [Session], devolvida pela função [sessionmaker] da linha 37;
  • linha 38: a classe [Session] é associada à base [dbpersonnes] através da cadeia de ligação da linha 14;
  • linha 43: é criada uma sessão. Como já foi referido, uma sessão pode ser comparada a uma transação;
  • linhas 45-46: o método [Session.execute] permite executar uma ordem SQL. Isto não é algo comum, uma vez que já foi referido que o ORM permitia evitar a linguagem SQL;
  • linhas 48-49: o método [metadata.create_all] permite criar todas as tabelas utilizando a instância [MetaData] da linha 17. Temos apenas uma: a tabela [personnes] definida nas linhas 20-26. O [sqlalchemy] irá utilizar a informação destas linhas para criar a tabela. Aqui reside uma das principais vantagens do ORM: oculta as especificidades das ordens SGBD. Com efeito, a ordem SQL [create] pode ser muito diferente de uma SGBD para outra, devido aos tipos atribuídos às colunas. Não houve uma padronização dos tipos de dados. Assim, a ordem varia de um para outro. Aqui, graças a isto:
    • descrevemos de forma única a tabela que pretendemos;
    • o [sqlalchemy] consegue gerar o [create] adequado para o SGBD que tem à sua frente;
  • linha 52: adicionamos um objeto [Personne] à sessão. Isto não o adiciona automaticamente à base de dados. Com efeito, um ORM segue as suas próprias regras para se sincronizar com a base de dados. Procura sempre otimizar o número de consultas que efetua. Vejamos um exemplo. O script adiciona (add) duas pessoas (pessoa1, pessoa2) à sessão e, em seguida, faz uma consulta: pretende ver todas as pessoas presentes na tabela. O [sqlalchemy] pode proceder da seguinte forma:
    • a adição de [personne1] pode ser feita na memória. Não há necessidade, por enquanto, de a inserir na base de dados;
    • o mesmo se aplica a [personne2];
    • segue-se a consulta do tipo [select]. É então necessário recuperar todas as linhas da tabela [personnes]. O [sqlalchemy] irá então inserir o [personne1, personne2] na base de dados e, em seguida, efetuar a consulta;

[sqlalchemy] irá, assim, realizar otimizações transparentes para o programador.

  • linha 56: para efetuar uma consulta do tipo [select] (quero ver…), utiliza-se o método [Session.query]. O parâmetro do método [query] é a classe mapeada com a tabela consultada. Este método devolve um tipo [Query]. O método [Query.all] solicita todos os objetos [Personne] da sessão. São-lhe devolvidas todas as linhas da tabela [personnes], cada uma na forma de um objeto [Personne]. Para o fazer, o [sqlalchemy] utiliza o mapeamento que foi definido entre a classe [Personne] e a tabela [personnes]. O resultado da linha 56 é uma lista de objetos [Personne];
  • linhas 58-61: são apresentados os elementos da lista [personnes]. Como a classe [Personne] deriva da classe [BaseEntity], o método [Personne.__str__] utilizado aqui implicitamente na linha 61 é, na verdade, o método [BaseEntity.__str__], que devolve a cadeia jSON do objeto chamador. Esta cadeia é a cadeia jSON do dicionário [Personne.asdict] (ver |BaseEntity|). Já referimos que, após o mapeamento, iríamos encontrar a propriedade [_sa_instance_state] em cada objeto [Personne]. No entanto, o valor desta propriedade não é do tipo [BaseEntity]. É, portanto, necessário excluí-la do dicionário da classe [Personne]; caso contrário, a visualização «trava». Foi isso que foi feito no script [config];
  • linhas 63-65: adicionam-se mais duas pessoas com o mesmo nome e apelido. No entanto, existe uma restrição de unicidade na união destas duas colunas. Por conseguinte, deveria ocorrer um erro. É isso que se pretende verificar;
  • linhas 67-68: solicitamos novamente a lista de todas as pessoas da base de dados;
  • linhas 70-73: e exibimo-las;
  • linhas 75-76: a sessão é validada («committed»). Como o próprio nome indica, a transação subjacente vai ser validada;
  • veremos, durante a execução, que as linhas 67-76 não serão executadas devido à exceção gerada pela linha 65. Passaremos então para as linhas 78-84 para tratar da exceção;
  • linha 78: a exceção [InterfaceError] ocorre se [sqlalchemy] não conseguir ligar-se à base de dados [dbpersonnes]. A exceção [IntegrityError] ocorre na linha 65;
  • linha 80: é exibido o erro;
  • linhas 82-84: se a sessão existir, é cancelada. Isto equivale a cancelar a transação subjacente;
  • linhas 85-88: em todos os casos, haja ou não erro, a sessão é encerrada para libertar recursos;

Os resultados da execução são os seguintes:


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
  • linhas 2-3: a lista de pessoas após a primeira inserção;
  • linha 5: a exceção [IntegrityError] que ocorreu quando foram adicionadas duas pessoas com o mesmo nome e apelido;
  • linhas 6-7: note-se a ordem SQL que falhou. Trata-se de uma ordem INSERT configurada: a ordem [sqlalchemy] inseriu as duas pessoas com uma única ordem INSERT. Vemos aqui que tentou otimizar as ordens SQL emitidas;

Agora vamos ver, com o phpMyAdmin, o conteúdo da tabela [personnes]:

Image

Vemos em [6] que a tabela está vazia. Nem sequer consta a primeira pessoa que o script tinha inserido na sessão. Isto porque a sessão decorria numa transação e esta foi revertida na cláusula [except] do script [main].

Vamos agora proceder à seguinte alteração no [main]:


    # uma inserção
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

Depois de adicionar uma pessoa na linha 2, removemos o comentário da linha 3. A operação [session.commit] irá validar a transação subjacente e uma nova transação será iniciada. Após a execução, o conteúdo da tabela [personnes] é o seguinte:

Image

Vemos em [6] que a primeira inserção foi mantida. Isto deve-se ao facto de ter sido efetuada no âmbito de uma transação 1 e de o erro que se seguiu ter ocorrido no âmbito de uma transação 2.

19.3. Scripts 02: os mapeamentos de [sqlalchemy]

Image

Os scripts 02 são uma variante dos scripts 01. Procuramos efetuar o máximo de configurações no [config.py]. Configuramos agora o ambiente [sqlalchemy] da aplicação:


def configure():
    # caminho absoluto de referência dos caminhos relativos da configuração
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # caminhos absolutos das dependências
    absolute_dependencies = [
        # BaseEntity, MyException, Pessoa, Utilitários
        f"{root_dir}/classes/02/entities",
    ]

    # define-se o syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # importações
    from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
    from sqlalchemy.orm import mapper, sessionmaker

    # ligação a uma base de dados MySQL
    engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

    # metadados
    metadata = MetaData()

    # a tabela
    personnes_table = Table("personnes", metadata,
                            Column('id', Integer, primary_key=True),
                            Column('prenom', String(30), nullable=False),
                            Column("nom", String(30), nullable=False),
                            Column("age", Integer, nullable=False),
                            UniqueConstraint('nom''prenom', name='uix_1')
                            )

    # o mapeamento
    from Personne import Personne

    mapper(Personne, personnes_table, properties={
        'id': personnes_table.c.id,
        'nome próprio: personnes_table.c.prenom,
        'apelido: personnes_table.c.nom,
        'idade': personnes_table.c.age
    })

    # a fábrica de sessões
    Session = sessionmaker()
    Session.configure(bind=engine)

    # estas informações são inseridas na configuração
    config = {}
    config["Session"] = Session
    config["metadata"] = metadata
    config["engine"] = engine
    config["personnes_table"] = personnes_table

    # configuração das classes
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # carregamos a configuração
    return config

Comentários

  • linhas 2-12: configuração do Python Path;
  • linhas 14-45: configuração do ambiente [sqlalchemy];
  • linhas 47-52: o ambiente [sqlalchemy] é adicionado ao dicionário de configuração;
  • linhas 54-56: configuração da classe [Personne];

Com esta configuração, o script [main] passa a ter o seguinte aspeto:


# configuramos a aplicação
import config

config = config.configure()

# o syspath está configurado — faz-se as importações
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError

from Personne import Personne

session = None
try:
    # uma sessão
    session = config["Session"]()

    # eliminação da tabela [personnes]
    session.execute("drop table if exists personnes")

    # recriação da tabela a partir do mapeamento
    config["metadata"].create_all(config["engine"])

    # duas inserções
    session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
    personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
    session.add(personne)

    # validação das duas inserções
    session.commit()

    # uma consulta
    personnes = session.query(Personne).all()

    # exibição
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # duas outras inserções, das quais a segunda falha
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))

    # uma consulta
    personnes = session.query(Personne).all()

    # exibição
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # validação da sessão
    session.commit()

except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
    # exibição
    print(f"L'erreur suivante s'est produite : {erreur}")
    # cancelamento da última sessão
    if session:
        print("rollback...")
        session.rollback()
finally:
    # exibição
    print("Travail terminé...")
    # libertação dos recursos da sessão
    if session:
        session.close()

Os resultados da execução são os seguintes:


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

Em phpMyAdmin, a tabela [personnes] passou a ter o seguinte aspeto:

Image

Agora, vejamos a tabela [personnes] gerada por [sqlalchemy]:

Image

  • em [6], os tipos utilizados para as diferentes colunas;
  • em [7], vemos que a coluna [id] tem o atributo [AUTO_INCREMENT]. Isto significa que, ao inserir uma linha na tabela, se essa linha não tiver um valor para a coluna [id], este será gerado por MySQL de forma incremental: 1, 2, 3, … Esta propriedade evita que tenhamos de nos preocupar com o valor da chave primária quando efetuamos uma inserção na tabela: deixamos que a MySQL a gere;
  • em [8], vemos que a coluna [id] é a chave primária;
  • em [9], encontramos a restrição de unicidade nos campos [nom, prenom];

19.4. Scripts 03: manipulação das entidades da sessão [sqlalchemy]

Image

O ficheiro de configuração [config] é o mesmo que no exemplo anterior. No script [main], realizam-se as operações clássicas [INSERT, UPDATE, DELETE, SELECT] na tabela [personnes] utilizando os métodos de [sqlalchemy]:


# configuração da aplicação
import config

config = config.configure()

# importações
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne

# exibe o conteúdo da tabela [personnes]
def affiche_table(session: Session):
    print("----------------")
    # uma consulta
    personnes = session.query(Personne).all()
    # visualização
    affiche_personnes(personnes)

# exibe uma lista de pessoas
def affiche_personnes(personnes: list):
    print("----------------")
    # visualização
    for personne in personnes:
        print(personne)


# principal ---------------------------
session = None
try:
    # uma sessão
    session = config["Session"]()

    # eliminação da tabela [personnes]
    # checkfirst=True: verifica primeiro se a tabela existe
    config["personnes_table"].drop(config["engine"], checkfirst=True)

    # recriação da tabela a partir do mapeamento
    config["metadata"].create_all(config["engine"])

    # inserções
    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}))

    # exibe-se o conteúdo da sessão
    affiche_table(session)

    # lista de pessoas por ordem alfabética dos apelidos e, em caso de igualdade de apelidos, por ordem alfabética dos nomes próprios
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # exibição
    affiche_personnes(personnes)

    # lista de pessoas com idade no intervalo [20,40], por ordem decrescente de idade
    # e, em caso de idades iguais, por ordem alfabética dos apelidos e, em caso de apelidos iguais, por ordem alfabética dos nomes próprios
    personnes = session.query(Personne). \
        filter(Personne.âge >= 20, Personne.âge <= 40). \
        order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())

    # exibição
    affiche_personnes(personnes)

    # inserção da Sra. Bruneau
    bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
    session.add(bruneau)
    # alteração da sua idade
    bruneau.âge = 47

    # lista de pessoas com o apelido Bruneau
    personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()

    # visualização
    affiche_personnes([personne])

    # eliminação da Sra. Bruneau
    session.delete(personne)

    # lista de pessoas com o apelido Bruneau
    personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")

    # visualização
    affiche_personnes(personnes)

    # validação da sessão
    session.commit()

except (DatabaseError, InterfaceError, IntegrityError) as erreur:
    # visualização
    print(f"L'erreur suivante s'est produite : {erreur}")
    # anulação da última sessão
    if session:
        session.rollback()

finally:
    # visualização
    print("Travail terminé...")
    # libertação dos recursos da sessão
    if session:
        session.close()

Comentários

  • linhas 20-25: a função [affiche_personnes] apresenta os elementos de uma lista de pessoas;
  • linhas 12-18: a função [affiche_table] apresenta o conteúdo da tabela [personnes];
  • linhas 34-36: elimina-se a tabela [personnes]. Ao contrário das versões anteriores, não se utiliza uma ordem SQL, mas sim um método de [sqlalchemy]:
    • config["personnes_table"] é o objeto [Table] que descreve a tabela [personnes];
    • config["engine"] é a cadeia de ligação à base de dados [dbpersonnes];
    • o parâmetro denominado [checkfirst=True] determina que a operação só seja executada se a tabela [personnes] existir;
  • linhas 38-39: a tabela [personnes] é recriada;
  • linhas 41-44: são adicionadas três pessoas à sessão. Recorde-se que estas não são necessariamente inseridas de imediato na tabela [personnes]. Isso depende da estratégia da [sqlalchemy], que visa o desempenho;
  • linhas 46-47: o conteúdo da tabela [personnes] é apresentado. Se as inserções das três pessoas ainda não tivessem sido efetuadas, são agora inseridas devido a este pedido;
  • linhas 49-50: um exemplo de utilização do método [order_by], que permite apresentar os resultados de uma consulta numa determinada ordem. A sintaxe [order_by(critère1, critère2)] apresenta os resultados primeiro de acordo com o critério [critère1] e, quando as linhas apresentam o mesmo valor de [critère1], são então ordenadas de acordo com o critério [critère2]. É possível definir vários critérios desta forma;
  • linhas 55-59: introduzem o conceito de filtro com o método [filter]. A notação [filter(critère1, critère2)] estabelece uma relação lógica (ET) entre os critérios utilizados;
  • linhas 64-67: é iniciada uma nova sessão;
  • linhas 70-71: outro exemplo de consulta filtrada. A função [func.lower(param)] converte [param] em minúsculas. Existem, assim, outras funções disponíveis, designadas por [func.xx]. Na expressão da linha 71:
    • [session.query.filter] devolve uma lista de objetos [Personne];
    • [session.query.filter.first] devolve o primeiro elemento dessa lista;
  • linha 77: remove-se um elemento da sessão;
  • linha 86: a sessão é validada;

Os resultados da execução são os seguintes:

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
  • linhas 4-6: o conteúdo da sessão;
  • linhas 8-10: o conteúdo da sessão por ordem decrescente dos nomes;
  • linhas 12-13: o conteúdo da sessão para as pessoas cuja idade se situa no intervalo [20, 40];
  • linha 15: a pessoa com o nome «bruneau»;

Em phpMyAdmin, o conteúdo da tabela [personnes] no final da execução é o seguinte:

Image

19.5. Scripts 04: utilização de uma base de dados [PostgreSQL]

Image

A pasta [04] é uma cópia da pasta [03]. Altera-se apenas um elemento: a cadeia de ligação no ficheiro [config]:


    # ligação a uma base de dados PostgreSQL
    engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")

Agora, esta cadeia de ligação aponta para a base de dados [dbpersonnes] de um SGBD [PostgreSQL]. Deve-se notar a utilização do conector [psycopg2]. Este tem de estar instalado.

A execução do script [main] produz os seguintes 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

Com a ferramenta [pgAdmin] (ver parágrafo |pgAdmin|), a tabela [personnes] apresenta o seguinte estado:

Image

A tabela [personnes] foi gerada com o código SQL seguinte:

Image

  • em [4-5], verifica-se que a coluna [id] é a chave primária. Vê-se também que tem um valor por defeito [mot clé DEFAULT], o que faz com que, se se inserir uma linha sem chave primária, esta seja gerada pelo SGBD. Trata-se de um procedimento comum: deixa-se que o SGBD gere as chaves primárias;

Esta versão 05 dos scripts [sqlalchemy] ilustra bem a facilidade de passar de um SGBD para outro: bastou alterar a cadeia de ligação num script de configuração. Nada mais mudou. Se compararmos os tipos das colunas do [id, nom, prenom, age] acima com os da tabela MySQL do exemplo |02|, verificamos que são diferentes. O [sqlalchemy] adapta-as ao SGBD utilizado. Esta facilidade de adaptação a um novo SGBD é motivo suficiente para adotar o [sqlalchemy] ou outro ORM.

19.6. Scripts 05: exemplo completo

Image

O exemplo analisado é uma repetição do exemplo estudado no parágrafo |troiscouches-v01|. Esse exemplo apresentava uma arquitetura de três camadas [ui, métier, dao] que manipulava entidades [Classe, Elève, Matière, Note]. As entidades estavam codificadas de forma estática numa camada [dao]. Agora, vamos colocá-las numa base de dados. Utilizaremos duas camadas SGBD: MySQL e PostgreSQL.

19.6.1. A arquitetura da aplicação

A arquitetura da aplicação será a seguinte:

Image

  • No [1-3], encontram-se as camadas [ui, métier, dao] já presentes no exemplo |troiscouches-v01|. A camada [dao] comunica agora com a camada [ORM];
  • as camadas [1-5] são implementadas por código Python;

19.6.2. As bases de dados

Criamos uma base de dados MySQL denominada [dbecole], propriedade do utilizador [admecole], com a palavra-passe [mdpecole]. Para tal, seguimos o procedimento descrito no parágrafo |criação de uma base de dados|:

Image

Image

  • em [1], a base de dados [dbecole] sem as tabelas [3];
  • na base de dados [7], o utilizador [admecole] tem todos os privilégios sobre esta base de dados;

Fazemos o mesmo com a base SGBD e a PostgreSQL. Criamos uma base de dados denominada [dbecole], propriedade do utilizador [admecole], com a palavra-passe [mdpecole]. Para tal, seguimos o procedimento descrito no parágrafo |criação de uma base de dados|:

Image

  • em [1], a base de dados [dbecole];
  • em [2], o utilizador [admecole];
  • em [3-4], a base de dados [dbecole] é propriedade do utilizador [admecole];

19.6.3. As entidades manipuladas pela aplicação

Na aplicação |troiscouches v01|, as entidades manipuladas eram as seguintes (ver |entidades|). São estas entidades que serão armazenadas nas bases de dados anteriores. Não iremos duplicar estas entidades na nova aplicação. Iremos buscá-las onde já estão definidas.

A classe [Classe]:


# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da classe
        # nome: nome da classe
        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):
        # o nome deve ser uma cadeia de caracteres não vazia
        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")

A classe [Elève]:


# importações
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador do aluno
        # apelido: apelido do aluno
        # nome próprio: nome próprio do aluno
        # turma: turma do aluno
        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:
        # o apelido deve ser uma cadeia de caracteres não vazia
        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:
        # o nome próprio deve ser uma cadeia de caracteres não vazia
        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:
            # é esperado um tipo Classe
            if isinstance(value, Classe):
                self.__classe = value
            # ou um tipo «dict»
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # ou um 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}")

A classe [Matière]:


# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da disciplina
        # nome: nome da disciplina
        # coeficiente: coeficiente da disciplina
        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):
        # o nome deve ser uma cadeia de caracteres não vazia
        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):
        # o coeficiente deve ser um número real >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # erro?
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

A classe [Note]:


# importações
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # atributos excluídos do estado da classe
    excluded_keys = []

    # propriedades da classe
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador da nota
        # valor: a própria nota
        # aluno: aluno (do tipo Aluno) a quem a nota diz respeito
        # disciplina: disciplina (do tipo Disciplina) a que a nota se refere
        # o objeto «Nota» corresponde, portanto, à nota de um aluno numa disciplina
        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):
        # a nota deve ser um número real entre 0 e 20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # espera-se um tipo «Aluno»
            if isinstance(value, Elève):
                self.__élève = value
            # ou um tipo «dict»
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # ou um 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:
            # é esperado um tipo «Matière»
            if isinstance(value, Matière):
                self.__matière = value
            # ou um tipo «dict»
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # ou um 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. Configuração

Image

A configuração foi dividida em vários ficheiros:

  • a configuração geral no ficheiro [config.py]: define o Python Path da aplicação e instancia as camadas da arquitetura;
  • a configuração de [sqlalchemy] em [config_database]: realiza os mapeamentos entre classes e tabelas;
  • as camadas da aplicação estão configuradas no ficheiro [config_layers];

O ficheiro [config] é o seguinte:


def configure(config: dict) -> dict:
    import os

    # passo 1 ---
    # define-se o Python Path da aplicação
    # caminho absoluto da pasta deste script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # caminho absoluto de referência para os caminhos relativos da configuração
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # caminhos absolutos das dependências
    absolute_dependencies = [
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # projeto de três camadas v01
        f"{root_dir}/troiscouches/v01/interfaces",
        f"{root_dir}/troiscouches/v01/services",
        f"{root_dir}/troiscouches/v01/entities",
        # documentação do presente projeto
        script_dir,
        f"{script_dir}/../services",
    ]

    # atualização do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # etapa 2 ------
    # configuração da base de dados
    import config_database
    config = config_database.configure(config)

    # etapa 3 ------
    # instanciação das camadas da aplicação
    import config_layers
    config = config_layers.configure(config)

    # aplicamos a configuração
    return config
  • linhas 4-27: construção do Python Path da aplicação;
  • linhas 29-32: configuração do [sqlalchemy];
  • linhas 34-37: configuração das camadas da aplicação;

O ficheiro [config_database] é o seguinte:


def configure(config: dict) -> dict:
    # config['sgbd'] é o nome do SGBD utilizado
    # MySQL: MySQL
    # pgres: PostgreSQL

    # configuração do SQLAlchemy
    from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine

    from sqlalchemy.orm import mapper, relationship, sessionmaker

    # cadeias de ligação às bases de dados utilizadas
    engines = {
        'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
        'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
    }
    # cadeia de ligação à base de dados utilizada
    engine = create_engine(engines[config['sgbd']])

    # metadados
    metadata = MetaData()

    # as tabelas da base de dados
    tables = {}
    # as classes mapeadas
    from Classe import Classe
    from Elève import Elève
    from Note import Note
    from Matière import Matière

    # a tabela de classes
    tables['classes'] = classes_table = \
        Table("classes", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              )

    mapper(Classe, tables['classes'], properties={
        'id': classes_table.c.id,
        'nome: classes_table.c.nom
    })

    # a tabela dos alunos
    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),
              # um aluno pertence a uma turma
              Column('classe_id', Integer, ForeignKey('classes.id')),
              )
    # mapeamento
    mapper(Elève, tables['élèves'], properties={
        'id': élèves_table.c.id,
        'apelido: élèves_table.c.nom,
        'nome próprio': élèves_table.c.prénom,
        'turma: relationship(Turma, backref="alunos", lazy="select")
    })

    # o í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)
              )
    # mapeamento
    mapper(Matière, tables['matières'], properties={
        'id': matières_table.c.id,
        'nome': matières_table.c.nom,
        "coefficient": matières_table.c.coefficient
    })

    # a tabela de notas
    tables['notes'] = notes_table = \
        Table("notes", metadata,
              Column('id', Integer, primary_key=True),
              Column('valeur', Float, nullable=False),
              # uma nota é a de um aluno
              Column('élève_id', Integer, ForeignKey('élèves.id')),
              # uma nota é a de uma disciplina
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # mapeamento
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valor': notes_table.c.valeur,
        'aluno': relação(Aluno, backref="notas", lazy="select"),
        'disciplina': relationship(Disciplina, backref="notas", lazy="select")
    })

    # configuração das 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']

    # a fábrica de sessões
    Session = sessionmaker()
    Session.configure(bind=engine)

    # uma sessão
    session = Session()

    # registam-se determinadas informações no dicionário de configuração
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # a configuração é carregada
    return config

Comentários

  • linhas 1-4: a função [configure] recebe um dicionário como parâmetro. Apenas a chave [sgbd] é utilizada. O seu valor é [mysql] se a base for uma base MySQL, e [pgres] se a base for uma base PostgreSQL;
  • linhas 6-9: importações de elementos da base [sqlalchemy]. O script [config_database] realiza os mapeamentos entre as tabelas da base de dados [dbecole] e as entidades [Classes, Elève, Matière, Note]. Na tabela, os dados da entidade estão encapsulados numa linha. No código Python, esses dados estão encapsulados num objeto. Daí o nome ORM (Object Relational Mapper): o ORM estabelece um mapeamento (uma ligação) entre as linhas de uma base de dados relacional e os objetos. Nesta aplicação, temos quatro entidades [Classe, Elève, Matière, Note] que serão ligadas a quatro tabelas [classes, élèves, matières, notes]. Note-se que os nomes das tabelas podem conter caracteres acentuados;
  • linhas 11-17: a cadeia de ligação à base de dados utilizada. Esta depende do elemento config[‘sgbd’];
  • linhas 24-28: as entidades da aplicação que serão objeto de um mapeamento [sqlalchemy]. Quando estas linhas forem executadas, o Python Path já terá sido estabelecido pelo script [config];
  • linhas 30-40: o mapeamento entre a entidade [Classe] e a tabela [classes];
  • linhas 30-35: a tabela [classes] é definida com a classe [Table] de [sqlalchemy]. Indicamos que esta tabela tem duas colunas:
    • a coluna [id], que é a chave primária e corresponde ao número da classe, linha 33;
    • a coluna [nom], que contém o nome da classe, linha 34;
  • linhas 31-32: repare que a sintaxe x=y=z é válida em Python: o valor de z é atribuído a y e, em seguida, o valor de y é atribuído a x;
  • linhas 37-40: enumeram-se as correspondências entre as colunas da tabela [classes] e as propriedades da entidade [Classe];
  • linhas 42-57: o mapeamento entre a entidade [Elève] e a tabela [élèves];
  • linhas 51-57: a tabela [élèves] é definida com a classe [Table] de [sqlalchemy]. Indicamos que esta tabela tem quatro colunas:
    • a coluna [id], que é a chave primária e corresponde ao número do aluno, linha 45;
    • a coluna [nom], que contém o apelido do aluno, linha 46;
    • a coluna [prénom], que contém o nome próprio do aluno, linha 47. Note-se que o nome de uma coluna pode conter caracteres acentuados;
    • linha 49, a coluna [classe_id], que conterá o número da turma a que o aluno pertence. A isto chama-se chave estrangeira. [élèves.classe_id] é uma chave estrangeira (ForeignKey) na coluna [classes.id]. Isto significa que o valor de [élèves.classe_id] deve existir na coluna [classes.id];
  • linhas 51-57: são listadas as correspondências entre as colunas da tabela [élèves] e as propriedades da entidade [Elève]:
    • as linhas 53-55 são fáceis de compreender;
    • a linha 56 é mais complexa: define o valor da propriedade [Elève.classe] como sendo calculado pela relação de chave estrangeira que liga as tabelas [élèves] e [classes]. Os parâmetros da função [relationship] são os seguintes:
      • [Classe]: é o nome da entidade com a qual a entidade [Elève] mantém uma relação de chave estrangeira. Esta relação deve concretizar-se na tabela [élèves] através da presença de uma chave estrangeira na tabela [classes]. Sabemos que esta existe;
      • [backref="élèves"]: o nome de uma propriedade que será adicionada à entidade [Classe]. [Classe.élèves] será a lista de todos os alunos da turma. Esta propriedade não deve já existir. Se já existir, basta escolher aqui outro nome para [backref]. O programador não precisa de gerir esta propriedade. Será a [sqlalchemy] que o fará. Basta que saiba que ela existe, adicionada pela [sqlalchemy], e que pode utilizá-la no seu código;
      • [lazy=’select’]: isto significa que o ORM não deve procurar atribuir imediatamente um valor à propriedade [Elève.classe]. Só deve procurar o seu valor quando o código o solicitar explicitamente. Assim:
      • se o código solicitar a lista de todos os alunos, estes serão apresentados, mas a sua propriedade [classe] não será calculada;
      • um pouco mais tarde, o código interessa-se por um aluno específico, [e], e faz referência à sua turma, [e.classe]. Esta referência irá, então, obrigar o [sqlalchemy] a efetuar uma consulta à base de dados para recuperar a turma do aluno, de forma transparente para o programador;
      • da mesma forma, a inclusão de [lazy=’select’] visa evitar consultas desnecessárias à base de dados;
  • linha 56: quando o ORM recupera uma linha da tabela [élèves], recupera as informações [id, nom, prénom, classe_id]. A partir daí, deve construir um objeto Aluno(id, apelido, nome próprio, turma). No que diz respeito às propriedades [id, nom, prénom], isso não apresenta dificuldades. Quanto à propriedade [classe], a situação é mais complicada. O seu valor é uma referência a um objeto do tipo [Classe]. No entanto, o ORM dispõe apenas de uma informação [élèves.classe_id]. Como [élèves.classe_id] é uma chave estrangeira na coluna [classes.id], indica-se aqui que utilize essa relação para recuperar, na tabela [classes], a linha com o ID=[élèves.classe_id] (que existe necessariamente) e de criar, a partir dessa linha, o objeto [Classe] esperado pela propriedade [Elève.classe];
  • linhas 59-71: o mapeamento entre a entidade [Matière] e a tabela [matières];
  • linhas 59-65: definição da tabela [sqlalchemy], denominada [matières];
  • linhas 66-71: enumeram-se as correspondências entre as colunas da tabela [matières] e as propriedades da entidade [Matière]. Não há dificuldades aqui;
  • linhas 73-90: o mapeamento entre a entidade [Note] e a tabela [notes];
  • linhas 73-82: definição da tabela [sqlalchemy], denominada [notes]. Possui duas chaves estrangeiras:
    • linha 79, a coluna [notes.élève_id] obtém os seus valores da coluna [élèves.id]]. Esta chave estrangeira reflete o facto de uma nota ser a nota de um aluno específico;
    • linha 81, a coluna [notes.matière_id] obtém os seus valores da coluna [matières.id]. Esta chave estrangeira representa o facto de uma nota ser uma nota numa disciplina específica;
  • linhas 84-90: o mapeamento entre a entidade [Note] e a tabela [notes]:
    • linha 88: a propriedade [Note.élève] deve ter como valor uma instância do tipo [Elève]. O ORM apenas contém, na linha da tabela [notes], a coluna [notes.élève_id], que faz referência à coluna [élèves.id]. Aqui, indica-se que se utilize esta relação de chave estrangeira para recuperar a instância [Elève] cuja nota se conhece. Além disso, [relationship(Elève, backref="notes", …)] irá criar a nova propriedade [Elève.notes], que será a lista de notas do aluno. Esta propriedade não deve já existir na classe [Elève];
    • linha 89: a propriedade [Note.matière] deve ter como valor uma instância do tipo [Matière]. A ORM contém, na linha da tabela [notes], apenas a coluna [notes.matière_id], que faz referência à coluna [matières.id]. Aqui, indica-se que se utilize esta relação de chave estrangeira para recuperar a instância [Matière], cuja nota já se conhece. Além disso, [relationship(Matière, backref="notes", …)] irá criar a nova propriedade [Matière.notes], que será a lista de notas na disciplina. Esta propriedade não deve já existir na classe [Matière];
  • linhas 92-96: define-se, para cada entidade derivada de [BaseEntity], a lista de propriedades a excluir do dicionário de propriedades da entidade (BaseEntity.asdict). Vimos que [sqlalchemy] adicionava a propriedade [_sa_instance_state] a todas as entidades mapeadas. Não a queremos no dicionário de propriedades. Além disso, vimos que os mapeamentos anteriores tinham adicionado novas propriedades às entidades:
    • [Elève.notes]: todas as notas do aluno;
    • [Classe.élèves]: todos os alunos da turma;
    • [Matière.notes]: todas as notas da disciplina;

Em geral, não queremos que estas propriedades sejam adicionadas ao estado da entidade. Com efeito, calcular o seu valor tem um custo SQL e esse valor é frequentemente desnecessário. Assim, se recuperarmos o aluno com o nome «X»:

  • (continuação)
    • o ORM irá devolver uma entidade [Elève(id, nom, prénom, classe, notes)]. Devido ao [lazy=’select’], as propriedades [classe, notes] associadas a chaves estrangeiras da base de dados não terão sido calculadas;
    • agora, se eu exibir a cadeia jSON deste aluno, sabemos que será a cadeia jSON do dicionário [asdict] da entidade. Se as propriedades [classe] e [notes] estiverem presentes, a [sqlalchemy] será obrigada a efetuar consultas à SQL para calcular os seus valores. Isto é dispendioso. Se for possível evitar essas consultas, é preferível;
    • aqui, excluímos todas as propriedades ligadas a uma chave estrangeira;
  • linhas 98-100: instanciação e configuração de um [Session factory] (factory = fábrica de produção). O objeto [Session] serve para criar sessões [sqlalchemy] associadas a transações;
  • linhas 102-103: criação de uma sessão sqlalchemy];
  • linha 106: alguns elementos da configuração [sqlalchemy] são inseridos no dicionário global da configuração da aplicação;
  • linha 109: este dicionário é devolvido;

O ficheiro [config_layers] configura as camadas da aplicação:


def configure(config: dict) -> dict:
    # instanciação da camada [dao]
    from DatabaseDao import DatabaseDao
    dao = DatabaseDao(config)

    # instanciação da camada [métier]
    from Métier import Métier
    métier = Métier(dao)

    # instanciação da camada [ui]
    from Console import Console
    ui = Console(métier)

    # colocamos as camadas na configuração
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # carregamos a configuração
    return config
  • linha 1: a função [configure] recebe o dicionário da configuração global da aplicação;
  • linhas 2-12: as camadas da aplicação são instanciadas;
  • linhas 15-17: as referências das camadas são inseridas na configuração global;
  • linha 20: a nova configuração é devolvida;

19.6.5. A camada [dao] - 1

Image

É importante compreender que a camada [dao] [3] comunica com aORM [sqlalchemy] [4], configurada conforme descrito no parágrafo anterior. Das três camadas [ui, métier, dao] da aplicação |troiscouches v01|, apenas a camada [dao] deve ser reescrita. As camadas [ui, métier] são mantidas.

A implementação da camada [dao] foi colocada na pasta [services]:

Image

[InterfaceDatabaseDao] é a interface da camada [dao]:


from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao


class InterfaceDatabaseDao(InterfaceDao, ABC):

    # inicialização da base de dados
    @abstractmethod
    def init_database(self, data: dict):
        pass
  • linha 6: a interface [InterfaceDatabaseDao] deriva tanto da classe [ABC], para ser uma classe abstrata, como da interface [InterfaceDao] do projeto |troiscouches v01|;
  • linhas 8-11: adiciona-se o método [init_database] aos métodos herdados de [InterfaceDao]. A sua função será inicializar a base de dados com os dados do dicionário [data] que lhe são passados como parâmetro na linha 10;

Recorde-se que a interface [InterfaceDao] era a seguinte:


# importações
from abc import ABC, abstractmethod

# interface DAO
from Elève import Elève


class InterfaceDao(ABC):
    # lista de turmas
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # lista de alunos
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # lista de disciplinas
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

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

    # lista de notas de um aluno
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # procurar um aluno pelo seu ID
    @abstractmethod
    def get_élève_by_id(self: object, élève_id: int) -> Elève:
        pass

A implementação da camada [dao] é a seguinte:


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 dados = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

  • linha 11: a classe [DatabaseDao] implementa a interface [InterfaceDatabaseDao];
  • linhas 13-16: o construtor da classe. Recebe como parâmetro o dicionário da configuração da aplicação;
  • linha 15: guarda-se a configuração [sqlalchemy];
  • linha 16: armazena-se a sessão [sqlalchemy] através da qual se irá manipular a base de dados;
  • linha 18: o método [init_database] inicializa a base de dados com o dicionário [data];

O dicionário [data] é implementado pelo seguinte 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 as classes
    classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
    classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
    classes = [classe1, classe2]
    # as disciplinas
    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]
    # os alunos
    é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]
    # as notas dos alunos nas diferentes disciplinas
    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]
    # agrupa-se o conjunto
    data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
    # os dados são apresentados
    return data
  • linha 34: o dicionário que será passado para o método [init_database]. Este dicionário é composto pelas seguintes chaves (linha 32):
    • [élèves]: a lista de alunos;
    • [classes]: a lista de turmas;
    • [matières]: a lista das disciplinas;
    • [notes]: a lista das notas de todos os alunos em todas as disciplinas;

Voltemos ao método [init_database]:


def init_database(self, data: dict):
        # configuração da base de dados
        database = self.database
        engine = database['engine']
        metadata = database['metadata']
        tables = database['tables']

        try:
            # eliminação das tabelas existentes
            # checkfirst=True: verifica primeiro se a tabela 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)

            # recriação das tabelas a partir do mapeamento
            metadata.create_all(engine)

            # preenchimento das tabelas
            session = self.session

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

            # disciplinas
            matières = data["matières"]
            for matière in matières:
                session.add(matière)

            # alunos
            é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)

            # confirmação
            session.commit()
        except (DatabaseError, InterfaceError, IntegrityError) as erreur:
            # anulação da sessão
            if session:
                session.rollback()
            # a exceção é reenviada
            raise MyException(23, f"{erreur}")
  • linhas 3-6: recuperam-se informações da configuração da base de dados;
  • linhas 9-14: vimos que a configuração [sqlalchemy] tinha mapeado quatro entidades para quatro tabelas [élèves, matières, classes, notes]. Começamos por eliminar essas tabelas, caso existam;
  • linhas 16-17: recriamos as quatro tabelas que acabámos de eliminar;
  • linhas 22-25: colocamos todas as classes na sessão;
  • linhas 27-30: colocam-se todas as disciplinas na sessão;
  • linhas 32-35: colocamos todos os alunos na sessão;
  • linhas 37-40: inserimos todas as notas na sessão;
  • para efetuar estas adições, seguimos uma ordem. Começámos pelas entidades que não tinham relações com outras entidades e terminámos pelas que tinham. Assim, quando adicionamos os alunos à sessão, as turmas a que estes se referem já se encontram na sessão;
  • linha 43: a sessão [sqlalchemy] foi validada. Após esta operação, temos a certeza de que todos os dados da sessão foram sincronizados com a base de dados. Em outras palavras, foram introduzidos nas tabelas. Isto foi possível graças aos mapeamentos definidos na configuração do [sqlalchemy]. O [sqlalchemy] sabe como cada entidade deve ser armazenada nas tabelas. O [sqlalchemy] também gerou as chaves estrangeiras que as tabelas podem possuir;
  • linhas 44-49: se ocorrer um problema, a sessão [sqlalchemy] é cancelada e, na linha 49, é lançada uma exceção;

19.6.6. Inicialização da base de dados

Image

O script [main_init_database] inicializa a base de dados com o conteúdo do script [data.py]. O seu código é o seguinte:


# à espera de um parâmetro mysql ou 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()

# configurar a aplicação
import config
config = config.configure({'sgbd': sgbd})

# o syspath está configurado — é possível efetuar as importações
from MyException import MyException

# recuperam-se os dados a introduzir na base de dados
import data
data = data.configure()

# recuperar a camada [dao]
dao = config["dao"]

# ----------- principal
try:
    # criação e inicialização das tabelas da base de dados
    dao.init_database(data)
except MyException as ex:
    # exibe-se o erro
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # libertação dos recursos utilizados pela aplicação
    import shutdown
    shutdown.execute(config)
# fim
print("Travail terminé...")
  • linhas 1-11: o script espera um parâmetro [mysql] ou [pgres], consoante se pretenda inicializar uma base de dados MySQL ou PostgreSQL;
  • linhas 13-15: a aplicação está configurada para o SGBD passado como parâmetro;
  • linhas 20-22: recuperam-se os dados a introduzir na base de dados;
  • linha 25: a camada [dao] já foi instanciada e está acessível na configuração da aplicação;
  • linha 30: a base de dados é inicializada;
  • linhas 34-37: quer ocorra um erro ou não, libertam-se os recursos da aplicação utilizando o módulo [shutdown];

O módulo [shutdown.py] é o seguinte:


def execute(config: dict):
    # libertação dos recursos utilizados pela aplicação
    sqlalchemy_session = config['database']['session']
    if sqlalchemy_session:
        sqlalchemy_session.close()

A função [shutdown.execute] encerra a sessão [sqlalchemy] utilizada para inicializar a base de dados.

Criamos uma primeira configuração de execução (ver |configuração de execução|) para executar o [main_init_database] com o SGBD e o MySQL:

Image

Os resultados da execução desta configuração são os seguintes no phpMyAdmin:

Image

Image

Image

Para o SGBD e o [PostgreSQL], utilizamos a seguinte configuração de execução:

Image

Após a execução, os resultados no [pgAdmin] são os seguintes:

Image

Image

Image

É de salientar a facilidade com que foi possível alterar o SGBD.

19.6.7. A camada [dao] – 2

Voltamos à classe [DatabaseDao], que implementa a camada [dao]. Até agora, apenas mostrámos a implementação do método [init_database]. Passamos agora a mostrar a implementação dos restantes 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 dados = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

    # lista de todas as classes
    def get_classes(self: object) -> list:
        # consulta
        return self.session.query(Classe).all()

    # lista de todos os alunos
    def get_élèves(self: object) -> list:
        # consulta
        return self.session.query(Elève).all()

    # lista de todas as disciplinas
    def get_matières(self: object) -> list:
        # consulta
        return self.session.query(Matière).all()

    # lista das notas de todos os alunos
    def get_notes(self: object) -> list:
        # consulta
        return self.session.query(Note).all()

    # lista das notas de um aluno específico
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # procura-se o aluno — é lançada uma exceção se este não existir
        # permite-se que a consulta seja executada
        élève = self.get_élève_by_id(élève_id)
        # recuperam-se as notas (carregamento diferido)
        notes = élève.notes
        # retorna-se um dicionário
        return {"élève": élève, "notes": notes}

    # um aluno identificado pelo seu n.º
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # procura-se o aluno
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # Encontrou-se?
        if élèves:
            return élèves[0]
        else:
            raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")

    # um aluno identificado pelo seu nome
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # à procura do aluno
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # já o encontrámos?
        if élèves:
            return élèves[0]
        else:
            raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")

    # uma turma identificada pelo seu n.º
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # à procura da turma
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # já a encontrámos?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # uma turma identificada pelo seu nome
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # Estamos à procura da turma
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # já a encontrámos?
        if classes:
            return classes[0]
        else:
            raise MyException(14, f"La classe de nom {classe_name} n'existe pas")

    # uma disciplina identificada pelo seu n.º
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # procura-se a disciplina
        matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
        # já foi encontrada?
        if matières:
            return matières[0]
        else:
            raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")

    # um material identificado pelo seu nome
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # Estamos à procura da matéria
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # já foi encontrada?
        if matières:
            return matières[0]
        else:
            raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
  • linhas 21-24: o método [get_classes] deve devolver a lista das turmas da escola. Na linha 20, utilizamos uma consulta já apresentada;
  • linhas 26-39: três outros métodos semelhantes para obter as listas de alunos, disciplinas e notas;
  • linhas 51-59: o método [get_élève_by_id] deve apresentar um aluno identificado pelo seu n.º. Lança uma exceção se este não existir;
    • linha 54: utiliza-se uma consulta filtrada. Obtém-se uma lista vazia ou com um elemento;
    • linha 57: se a lista recuperada não estiver vazia, devolve-se o primeiro elemento da lista;
    • caso contrário, na linha 59, é lançada uma exceção;
  • linhas 41-49: o método [get_notes_for_élève_by_id] deve devolver as notas de um aluno identificado pelo seu n.º:
    • linha 45: utiliza-se o método [get_élève_by_id] para obter a entidade «Aluno» do aluno;
    • na linha 47, utiliza-se a propriedade [Elève.notes] criada pelo mapeamento entre a entidade [Note] e a tabela [notes] (ver parágrafo |configuração do SQLAlchemy|) e que representa as notas do aluno;
    • linha 49: devolve-se um dicionário;
  • linhas 61-109: uma série de métodos semelhantes que permitem:
    • encontrar um aluno pelo nome, linhas 61-69;
    • encontrar uma turma, linhas 71-89;
    • localizar uma disciplina, linhas 91-109;

19.6.8. O script [main_joined_queries]

Image

O script [main_joined_queries] tem este nome porque se destina a destacar as consultas feitas implicitamente pelo [sqlalchemy] para recuperar informações pertencentes a várias tabelas. Estas consultas, ocultas ao programador, são efetuadas sempre que uma propriedade de uma entidade é associada à função [relationship] no mapeamento da entidade. Por exemplo:


    # mapeamento
    mapper(Note, tables['notes'], properties={
        'id': notes_table.c.id,
        'valor: notes_table.c.valeur,
        'aluno': relação(Aluno, backref="notas", lazy="select"),
        'disciplina': relationship(Disciplina, backref="notas", lazy="select")
    })

Acima, o mapeamento entre a entidade [Note] e a tabela [notes]:

  • na linha 5, quando a propriedade [élève] de uma entidade [Note] for solicitada pela primeira vez, será procurada na tabela [élèves] através de uma consulta SQL. Enquanto esta propriedade não for solicitada, permanece indefinida (carregamento diferido). Depois de obtida, o seu valor permanece na memória da ORM. Quando for referenciada uma segunda vez, a ORM fornecerá imediatamente o seu valor sem passar por uma nova consulta SQL. Tudo isto é transparente para o programador;
  • o mesmo se aplica à propriedade inversa [Elève.notes] (backref), linha 5;
  • o mesmo se aplica à propriedade [Note.matière] e à sua propriedade inversa [Matière.notes] (backref), linha 6;

O script [main_joined_queries] é o seguinte:


# é esperado um parâmetro mysql ou 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()

# configurar a aplicação
import config
config = config.configure({"sgbd": sgbd})

# o syspath está configurado — já se podem fazer as importações
from MyException import MyException

# a camada [dao]
dao = config["dao"]
try:
    # aluno por ID
    print("élève id=11 -----------")
    élève = dao.get_élève_by_id(11)
    print(f"élève={élève}")
    # a turma do aluno (carregamento diferido)
    classe = élève.classe
    print(f"classe de l'élève : {classe}")
    # os alunos da mesma turma (carregamento diferido)
    print("élèves dans la même classe :")
    for élève in classe.élèves:
        print(f"élève={élève}")

    # um aluno pelo nome
    print("élève nom='nom2' -----------")
    print(f"élève={dao.get_élève_by_name('nom2')}")
    # a sua turma (carregamento diferido)
    print(f"classe de l'élève : {élève.classe}")

    # notas de um aluno
    print("notes de l'élève id=11 -----------")
    # primeiro o aluno
    élève = dao.get_élève_by_id(11)
    # depois as suas notas (carregamento diferido)
    for note in élève.notes:
        # a nota
        print(f"note={note}, "
              # a disciplina da nota (carregamento diferido)
              f"matière={note.matière}")

    # os alunos de uma turma
    print("élèves de la classe nom='classe1' -----------")
    # primeiro a turma
    classe = dao.get_classe_by_name('classe1')
    # depois os alunos (carregamento diferido)
    for élève in classe.élèves:
        print(élève)

    # o mesmo se aplica a [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)

    # as notas numa disciplina
    print("matière de nom='matière1' -----------")
    # primeiro a disciplina
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # depois as notas nessa disciplina (carregamento diferido)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # o mesmo para a disciplina 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:
    # é apresentado o erro
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # exibe-se o erro
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # liberam-se os recursos
    import shutdown
    shutdown.execute(config)

Os comentários são suficientes para compreender o código.

Cria-se uma configuração de execução para MySQL:

Image

Os resultados da execução são os seguintes:

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 compreender estes resultados, é necessário ter em conta que foram excluídas algumas propriedades do dicionário de entidades (ver |configuração|):


    # configuração das 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']

Assim, quando escrevemos [print(f"élève={élève}")] na linha 26 do código, a linha 1 acima indica-nos que as propriedades ['_sa_instance_state', 'notes', 'classe'] não serão apresentadas. É isso que se observa na linha 3 dos resultados. Todas as outras propriedades são apresentadas. Assim, ainda na linha 3, descobrimos uma nova propriedade [classe_id] que inicialmente não existia na entidade [Elève]. Esta propriedade corresponde diretamente à coluna [classe_id] da tabela [élèves]. Assim, [sqlalchemy] adicionou as seguintes propriedades à entidade [Elève]: [classe_id, _sa_instance_state, notes]. É importante ter isto em conta, sobretudo porque estas propriedades não devem já existir na entidade mapeada.

As propriedades excluídas do dicionário de entidades são importantes. Se, por exemplo, não se excluírem as propriedades [notes, élève] da entidade [Elève], então a operação [print(f"élève={élève}")] irá exibi-las e, consequentemente, tal como acabou de ser explicado, provocar consultas SQL implícitas (carregamento diferido) para recuperar os valores dessas propriedades. Se, como neste caso, for apresentada uma lista de alunos, as operações implícitas SQL são realizadas para cada aluno. Isto pode ser, por um lado, desnecessário e, por outro, certamente dispendioso em termos de tempo de execução.

Para executar o script com uma base PostgreSQL, cria-se a seguinte configuração de execução:

Image

A execução produz os mesmos resultados que com MySQL.

19.6.9. O script [main_stats_for_élève]

Image

O script [main_stats_for_élève] é o que já foi utilizado na aplicação |troiscouches v01]. Na altura, chamava-se [main]. Trata-se de uma aplicação de consola que permite obter determinados indicadores sobre as notas de um aluno: [moyenne pondérée, min, max, liste]. Inser-se na seguinte arquitetura:

Image

Nesta arquitetura em camadas, apenas a camada [dao] foi alterada entre a aplicação |troiscouches v01| e esta. Como a nova camada [dao] respeita a interface [InterfaceDao] da antiga camada [dao], as camadas [ui, métier] não precisam de ser alteradas. Por isso, é possível continuar a utilizar as camadas definidas na aplicação |troiscouches v01|.

O script [main_stats_for_élève] implementa a camada [main] do esquema acima da seguinte forma:


# aguarda-se um parâmetro mysql ou 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()

# a aplicação está a ser configurada
import config
config = config.configure({'sgbd': sgbd})

# o syspath está configurado — já é possível efetuar as importações
from MyException import MyException

# a camada [ui]
ui = config["ui"]
try:
    # Execução da camada [ui]
    ui.run()
except MyException as ex1:
    # é apresentado o erro
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # é exibido o erro
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # os recursos são libertados
    import shutdown
    shutdown.execute(config)
  • linha 20: obtém-se uma referência à camada [ui] na configuração da aplicação;
  • linha 24: inicia-se o diálogo com o utilizador utilizando o único método da camada [ui];

Uma configuração de execução para PostgreSQL seria a seguinte:

Image

Eis um exemplo de execução com esta configuração:


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