Skip to content

19. Utilização do ORM SQLALCHEMY

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

Image

Neste capítulo, utilizaremos o ORM (Mapeador Objeto-Relacional) [sqlalchemy] para acessar os SGBD de maneira uniforme, independentemente do SGBD utilizado. Um ORM permite duas coisas:

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

A arquitetura passa a ser a seguinte:

O script agora está separado dos conectores pelo ORM. Ele se comunica com o ORM por meio de classes e métodos. Ele não executa código SQL. É o ORM que faz isso com os conectores aos quais está ligado. Ele oculta do script as particularidades desses conectores. Assim, o código do script é insensível a uma mudança de conector (portanto, do SGBD);

A estrutura 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 em um 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. Esses scripts utilizarão as classes de [2]: BaseEntity, MyException, Pessoa, Utilitários;

19.2.1. Configuração

O arquivo [config] configura o aplicativo da seguinte maneira:


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, Ferramentas
        f"{root_dir}/classes/02/entities",
    ]

    # definimos 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']

    # carregando 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 do aplicativo;
  • linhas 16-17: talvez você se lembre de que a classe |BaseEntity| possui um atributo de classe chamado [excluded_keys]. Esse atributo é uma lista na qual se colocam as propriedades da classe que não se deseja 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 o motivo;

19.2.2. Script [démo]

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


# recupera-se a configuração do aplicativo
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': personnes_table.c.prenom,
    'sobrenome': 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 modificada — ela foi “enriquecida”
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

Comentários

  • linhas 1-4: configuramos o aplicativo;
  • 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]. Ela permite descrever uma tabela de um banco de dados. Aqui, vamos descrever a tabela [personnes] do banco de dados MySQL [dbpersonnes] estudado 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 a seguir 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 tipo da coluna;
      • 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 necessariamente ter um valor quando uma linha é inserida na tabela;
    • linha 21: por fim, a classe [UniqueConstraint] permite descrever uma restrição de exclusividade. Aqui, indica-se que as colunas (sobrenome, nome) devem ser únicas na tabela. A propriedade denominada [name] permite atribuir um nome a essa restrição. Aqui, é preciso distinguir dois casos:
      • estamos descrevendo uma tabela existente. Nesse caso, é preciso procurar o nome da restrição nas propriedades da tabela (phpMyAdmin ou pgAdmin);
      • estamos descrevendo uma tabela que será criada. Nesse caso, inserimos o nome desejado;
  • linhas 23-25: criamos uma pessoa [personne1] e exibimos seu dicionário [__dict__]. Teremos aqui:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • linhas 27-33: fazemos um mapeamento, ou seja, criamos 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 realiza o mapeamento;
    • linha 28: o segundo parâmetro é a tabela à qual ela 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 no qual as chaves são as propriedades da classe mapeada e os valores, as colunas da tabela mapeada. Para designar a coluna X da tabela [personnes_table], escreve-se [personnes_table.c.X];
  • linhas 35-36: exibimos novamente a pessoa [personne1] após a conclusão do mapeamento. Observamos que ela 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 ela é exibida. 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}

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

  • (continuação)
    • uma nova propriedade [_sa_instance_state] aparece. Percebe-se que se trata de um objeto do ORM [sqlalchemy];
    • as demais propriedades tiveram removido o prefixo que indicava a qual classe pertenciam;

Portanto, pode-se concluir que a operação de mapeamento das linhas 27 a 33 modificou a classe [Personne].

Quando quisermos exibir o status de um objeto [Personne], geralmente não precisaremos da propriedade [_sa_instance_state]. Ela existe, na verdade, apenas para o funcionamento interno do [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] do banco de dados MySQL [dbpersonnes], interagindo com [sqlalchemy]. Para entender o que vem a seguir, é preciso lembrar a arquitetura utilizada aqui:

Image

Se [Database1] é a base [dbpersonnes], percebe-se que a conexão entre o script e essa base passa por duas entidades:

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

O script [main] se comunicará com o ORM, que, por sua vez, se comunicará com o conector Python. O ORM se comunica com esse conector por meio das ferramentas descritas nos parágrafos |MySQL| e |PostgreSQL|, notadamente emitindo comandos SQL. O script [main] não utilizará comandos SQL. Ele se baseará na API (Interface de Programação de Aplicativos) do ORM, composta por classes e interfaces.

O script [main] é o seguinte:


# configuramos o aplicativo
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 conexão com um banco 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': personnes_table.c.prenom,
    'sobrenome': 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()

    # exclusã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, sendo que a segunda falha devido à regra de exclusividade (nome, sobrenome)
    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()

    # exibiçã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:
    # liberaçã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 string de conexão com o banco de dados. Ela especifica:
    • o SGBD utilizado (mysql);
    • o conector Python utilizado (mysql.connector sem o ponto);
    • o usuário que se conecta (admpersonnes);
    • sua senha (nobody);
    • a máquina na qual está o SGBD (localhost = máquina na qual está o script que está sendo executado);
    • o nome do banco de dados (dbpersonnes);

Com essas informações, o [sqlalchemy] pode se conectar ao banco de dados. Observe que o conector Python utilizado já deve estar instalado. O [sqlalchemy] não faz isso.

  • 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 em uma 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], retornada pela função [sessionmaker] da linha 37;
  • linha 38: a classe [Session] é associada à base [dbpersonnes] por meio da cadeia de conexão da linha 14;
  • linha 43: cria-se uma sessão. Como já foi mencionado, pode-se comparar uma sessão a uma transação;
  • linhas 45-46: o método [Session.execute] permite executar uma ordem SQL. Isso não é algo comum, já que foi dito 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] utilizará as informações dessas linhas para criar a tabela. Aqui está uma das principais vantagens do ORM: ela oculta as especificidades das ordens SGBD. De fato, a ordem SQL [create] pode ser muito diferente de uma SGBD para outra devido aos tipos atribuídos às colunas. Não houve padronização dos tipos de dados. Assim, a ordem varia de um para outro. Aqui, graças a:
    • descrevemos de forma única a tabela que desejamos;
    • o [sqlalchemy] consegue gerar o [create] adequado para o SGBD que tem à sua frente;
  • linha 52: adicionamos um objeto [Personne] à sessão. Isso não o adiciona automaticamente ao banco de dados. Na verdade, um ORM segue suas próprias regras para se sincronizar com o banco de dados. Ele sempre buscará otimizar o número de consultas que realiza. Vejamos um exemplo. O script adiciona (add) duas pessoas (pessoa1, pessoa2) à sessão e, em seguida, faz uma consulta: ele deseja 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 inseri-la no banco de dados;
    • o mesmo vale para [personne2];
    • em seguida, vem a consulta do tipo [select]. É preciso, então, recuperar todas as linhas da tabela [personnes]. [sqlalchemy] irá, então, inserir [personne1, personne2] no banco de dados e, em seguida, executar a consulta;

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

  • linha 56: para fazer uma consulta do tipo [select] (quero ver…), utiliza-se o método [Session.query]. O parâmetro do método [query] é a classe mapeada à tabela consultada. Esse método retorna um tipo [Query]. O método [Query.all] solicita todos os objetos [Personne] da sessão. São retornadas a ele todas as linhas da tabela [personnes], cada uma na forma de um objeto [Personne]. Para fazer isso, o [sqlalchemy] utiliza o mapeamento que foi feito entre a classe [Personne] e a tabela [personnes]. O resultado da linha 56 é uma lista de objetos [Personne];
  • linhas 58-61: são exibidos 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 retorna a cadeia jSON do objeto chamador. Essa cadeia é a cadeia jSON do dicionário [Personne.asdict] (cf. |BaseEntity|). Dissemos que, após o mapeamento, encontraríamos a propriedade [_sa_instance_state] em cada objeto [Personne]. No entanto, o valor dessa propriedade não é do tipo [BaseEntity]. Portanto, é preciso excluí-la do dicionário da classe [Personne]; caso contrário, a exibição “trava”. Foi isso que foi feito no script [config];
  • linhas 63-65: adicionamos mais duas pessoas com o mesmo nome e sobrenome. No entanto, existe uma restrição de exclusividade na união dessas duas colunas. Portanto, deveria ocorrer um erro. É isso que estamos tentando verificar;
  • linhas 67-68: solicitamos novamente a lista de todas as pessoas do banco de dados;
  • linhas 70-73: e as exibimos;
  • linhas 75-76: a sessão é validada (“committed”). Como o próprio nome indica, a transação subjacente será validada;
  • veremos, na 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 a exceção;
  • linha 78: a exceção [InterfaceError] ocorre se [sqlalchemy] não conseguir se conectar ao banco de dados [dbpersonnes]. A exceção [IntegrityError] ocorre na linha 65;
  • linha 80: o erro é exibido;
  • linhas 82-84: se a sessão existir, ela é cancelada. Isso equivale a cancelar a transação subjacente;
  • linhas 85-88: em todos os casos, haja erro ou não, a sessão é encerrada para liberar 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 ao adicionar duas pessoas com o mesmo nome e sobrenome;
  • linhas 6-7: observe-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 ele tentou otimizar as ordens SQL emitidas;

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

Image

Vemos em [6] que a tabela está vazia. Não há nem mesmo a primeira pessoa que o script havia inserido na sessão. Isso ocorre porque a sessão estava ocorrendo em uma transação e essa transação foi revertida na cláusula [except] do script [main].

Vamos agora fazer a seguinte modificação no [main]:


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

Após adicionar uma pessoa na linha 2, removemos o comentário da linha 3. A operação [session.commit] 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. Isso se deve ao fato de que ela foi realizada dentro de uma transação 1 e que o erro subsequente ocorreu dentro de uma transação 2.

19.3. Scripts 02: os mapeamentos de [sqlalchemy]

Image

Os scripts 02 são uma variante dos scripts 01. Tentamos realizar o máximo possível de configurações no [config.py]. Agora, configuramos nele o ambiente [sqlalchemy] do aplicativo:


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",
    ]

    # definimos 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

    # link para um banco 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': personnes_table.c.prenom,
        'sobrenome': personnes_table.c.nom,
        'idade': personnes_table.c.age
    })

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

    # essas 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 essa configuração, o script [main] fica da seguinte forma:


# configura-se o aplicativo
import config

config = config.configure()

# o syspath está configurado — fazemos 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"]()

    # exclusã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, sendo que 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é...")
    # liberaçã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] ficou da seguinte forma:

Image

Agora, vamos examinar a tabela [personnes] gerada por [sqlalchemy]:

Image

  • em [6], os tipos utilizados para as diferentes colunas;
  • em [7], vemos que a coluna [id] possui o atributo [AUTO_INCREMENT]. Isso 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, … Essa propriedade nos poupa de nos preocuparmos com o valor da chave primária ao inserirmos dados na tabela: deixamos que a coluna MySQL o gere;
  • em [8], vemos que a coluna [id] é a chave primária;
  • em [9], encontramos a restrição de exclusividade nos campos [nom, prenom];

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

Image

O arquivo de configuração [config] é o mesmo do 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 do aplicativo
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()
    # exibição
    affiche_personnes(personnes)

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


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

    # exclusã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 o conteúdo da sessão
    affiche_table(session)

    # lista de pessoas em ordem alfabética pelos sobrenomes e, em caso de sobrenomes iguais, em ordem alfabética pelos 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], em ordem decrescente de idade
    # e, em caso de idades iguais, por ordem alfabética dos sobrenomes e, em caso de sobrenomes iguais, por ordem alfabética dos nomes
    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)

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

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

    # exibição
    affiche_personnes([personne])

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

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

    # visualização
    affiche_personnes(personnes)

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

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

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

Comentários

  • linhas 20-25: a função [affiche_personnes] exibe os elementos de uma lista de pessoas;
  • linhas 12-18: a função [affiche_table] exibe o conteúdo da tabela [personnes];
  • linhas 34-36: a tabela [personnes] é excluída. 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 string de conexão com o banco 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: três pessoas são adicionadas à sessão. Vale lembrar que elas não são necessariamente inseridas imediatamente na tabela [personnes]. Isso depende da estratégia do [sqlalchemy], que visa o desempenho;
  • linhas 46-47: o conteúdo da tabela [personnes] é exibido. Se as inserções das três pessoas ainda não tivessem sido feitas, elas são realizadas agora devido a essa consulta;
  • linhas 49-50: um exemplo de uso do método [order_by], que permite apresentar os resultados de uma consulta em uma determinada ordem. A sintaxe [order_by(critère1, critère2)] exibe os resultados primeiro de acordo com o critério [critère1] e, quando as linhas apresentam o mesmo valor de [critère1], elas são então classificadas de acordo com o critério [critère2]. É possível definir vários critérios dessa 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: uma nova pessoa é conectada;
  • linhas 70-71: outro exemplo de consulta filtrada. A função [func.lower(param)] converte [param] em letras minúsculas. Existem, portanto, outras funções disponíveis, como [func.xx]. Na expressão da linha 71:
    • [session.query.filter] retorna uma lista de objetos [Personne];
    • [session.query.filter.first] retorna 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 em ordem decrescente dos nomes;
  • linhas 12-13: o conteúdo da sessão para as pessoas cuja idade está no intervalo [20, 40];
  • linha 15: a pessoa chamada “bruneau”;

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

Image

19.5. Scripts 04: uso de um banco de dados [PostgreSQL]

Image

A pasta [04] é uma cópia da pasta [03]. Alteramos apenas um detalhe: a string de conexão no arquivo [config]:


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

Agora, essa string de conexão aponta para o banco de dados [dbpersonnes] de um SGBD [PostgreSQL]. Observe-se o uso do conector [psycopg2]. É necessário que este esteja 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 a seguir:

Image

  • em [4-5], percebe-se que a coluna [id] é a chave primária. Também se observa que ela possui um valor padrão [mot clé DEFAULT], o que faz com que, se for inserida uma linha sem chave primária, esta seja gerada pelo SGBD. Esse é um procedimento comum: deixa-se que o SGBD gere as chaves primárias;

Esta versão 05 dos scripts [sqlalchemy] demonstra bem a facilidade de alternar de um SGBD para outro: bastou alterar a string de conexão em um 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|, vemos que eles são diferentes. O [sqlalchemy] os adapta ao SGBD utilizado. Essa 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 versão revisada daquele 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 em uma camada [dao]. Agora, vamos colocá-las em um banco de dados. Utilizaremos duas camadas SGBD: MySQL e PostgreSQL.

19.6.1. A arquitetura do aplicativo

A arquitetura do aplicativo será a seguinte:

Image

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

19.6.2. Os bancos de dados

Estamos criando um banco de dados MySQL denominado [dbecole], de propriedade do usuário [admecole], com a senha [mdpecole]. Para isso, seguimos o procedimento descrito no parágrafo |criação de um banco de dados|:

Image

Image

  • em [1], o banco de dados [dbecole] sem as tabelas [3];
  • no [7], o usuário [admecole] possui todos os privilégios sobre esse banco de dados;

Fazemos o mesmo com SGBD e PostgreSQL. Criamos um banco de dados chamado [dbecole], de propriedade do usuário [admecole], cuja senha é [mdpecole]. Para isso, seguimos o procedimento descrito no parágrafo |criação de um banco de dados|:

Image

  • em [1], o banco de dados [dbecole];
  • em [2], o usuário [admecole];
  • em [3-4], o banco de dados [dbecole] pertence ao usuário [admecole];

19.6.3. As entidades manipuladas pelo aplicativo

No aplicativo |troiscouches v01|, as entidades manipuladas eram as seguintes (ver |entidades|). São essas entidades que serão armazenadas nos bancos de dados anteriores. Não duplicaremos essas entidades no novo aplicativo. 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 sequência 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
        # sobrenome: sobrenome 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 sobrenome deve ser uma sequência 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 deve ser uma sequência 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:
            # espera-se 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 sequência 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) ao qual a nota se refere
        # disciplina: disciplina (do tipo Disciplina) à qual a nota se refere
        # o objeto Nota é, portanto, a nota de um aluno em uma 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:
            # espera-se 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 arquivos:

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

O arquivo [config] é o seguinte:


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

    # etapa 1 ---
    # define-se o Python Path do aplicativo
    # 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 do banco 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)

    # fazemos 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 do aplicativo;

O arquivo [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 conexão aos bancos de dados em uso
    engines = {
        'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
        'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
    }
    # cadeia de conexão com o banco de dados em uso
    engine = create_engine(engines[config['sgbd']])

    # metadados
    metadata = MetaData()

    # as tabelas do banco 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,
        'sobrenome': élèves_table.c.nom,
        'nome': é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 é 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="selecionar"),
        '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()

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

    # a configuração é restaurada
    return config

Comentários

  • linhas 1-4: a função [configure] recebe um dicionário como parâmetro. Apenas a chave [sgbd] é utilizada. 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 de [sqlalchemy]. O script [config_database] realiza os mapeamentos entre as tabelas do banco de dados [dbecole] e as entidades [Classes, Elève, Matière, Note]. Na tabela, os dados da entidade estão encapsulados em uma linha. No código Python, eles são encapsulados em um objeto. Daí o nome ORM (Object Relational Mapper): o ORM realiza um mapeamento (uma ligação) entre as linhas de um banco 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]. Observe que os nomes das tabelas podem conter caracteres acentuados;
  • linhas 11-17: a string de conexão com o banco de dados utilizado. Ela depende do elemento config[‘sgbd’];
  • linhas 24-28: as entidades da aplicação que serão objeto de um mapeamento [sqlalchemy]. Quando essas 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 essa tabela possui 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: observe 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: listam-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 essa tabela possui 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 sobrenome do aluno, linha 46;
    • a coluna [prénom], que contém o nome próprio do aluno, linha 47. Observe que o nome de uma coluna pode conter caracteres acentuados;
    • linha 49, a coluna [classe_id], que conterá o número da turma à qual o aluno pertence. Isso é chamado de chave estrangeira. [élèves.classe_id] é uma chave estrangeira (ForeignKey) na coluna [classes.id]. Isso significa que o valor de [élèves.classe_id] deve existir na coluna [classes.id];
  • linhas 51-57: listam-se 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 entender;
    • a linha 56 é mais complexa: ela 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]: esse é o nome da entidade com a qual a entidade [Elève] mantém uma relação de chave estrangeira. Essa relação deve se concretizar na tabela [élèves] por meio da presença de uma chave estrangeira na tabela [classes]. Sabemos que ela 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. Essa propriedade não deve existir ainda. Se já existir, basta escolher outro nome aqui para [backref]. O desenvolvedor não precisa gerenciar essa propriedade. Isso será feito por [sqlalchemy]. Ele precisa apenas saber que ela existe, adicionada por [sqlalchemy], e que pode utilizá-la em seu código;
      • [lazy=’select’]: isso significa que o ORM não deve tentar atribuir imediatamente um valor à propriedade [Elève.classe]. Ele só deve buscar seu valor quando o código solicitar explicitamente. Assim:
      • se o código solicitar a lista de todos os alunos, eles serão retornados, mas sua propriedade [classe] não será calculada;
      • um pouco mais tarde, o código se interessa por um aluno específico, [e], e faz referência à sua turma, [e.classe]. Essa referência forçará, então, o [sqlalchemy] a fazer uma consulta ao banco de dados para recuperar a turma do aluno, de forma transparente para o desenvolvedor;
      • além disso, a inclusão de [lazy=’select’] visa evitar consultas desnecessárias ao banco de dados;
  • linha 56: quando o ORM recupera uma linha da tabela [élèves], ele obtém as informações [id, nom, prénom, classe_id]. A partir daí, ele deve construir um objeto Aluno(id, sobrenome, nome, turma). Para as propriedades [id, nom, prénom], isso não apresenta dificuldades. Para a propriedade [classe], é mais complicado. Seu valor é uma referência a um objeto do tipo [Classe]. No entanto, o ORM possui apenas uma informação [élèves.classe_id]. Como [élèves.classe_id] é uma chave estrangeira na coluna [classes.id], instruímos aqui que utilize essa relação para recuperar, na tabela [classes], a linha com o ID = [élèves.classe_id] (que necessariamente existe) e, a partir dessa linha, criar 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: listam-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]. Ela possui duas chaves estrangeiras:
    • linha 79, a coluna [notes.élève_id] obtém seus valores da coluna [élèves.id]]. Essa chave estrangeira representa o fato de que uma nota pertence a um aluno específico;
    • linha 81, a coluna [notes.matière_id] obtém seus valores da coluna [matières.id]. Essa chave estrangeira representa o fato de que uma nota é uma nota em uma 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]. A tabela ORM contém, na linha da tabela [notes], apenas a coluna [notes.élève_id], que faz referência à coluna [élèves.id]. Aqui, recomenda-se utilizar essa relação de chave estrangeira para localizar a instância [Elève], cuja nota já é conhecida. Além disso, [relationship(Elève, backref="notes", …)] criará a nova propriedade [Elève.notes], que será a lista de notas do aluno. Essa 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 possui, na linha da tabela [notes], apenas a coluna [notes.matière_id], que faz referência à coluna [matières.id]. Recomenda-se aqui utilizar essa relação de chave estrangeira para localizar a instância [Matière], cuja nota já está disponível. Além disso, [relationship(Matière, backref="notes", …)] criará a nova propriedade [Matière.notes], que será a lista de notas na disciplina. Essa 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 serem excluídas 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 queremos essa propriedade no dicionário de propriedades. Além disso, vimos que os mapeamentos anteriores haviam 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 essas propriedades sejam adicionadas ao estado da entidade. De fato, calcular seu valor tem um custo SQL e esse valor costuma ser desnecessário. Assim, se recuperarmos o aluno chamado ‘X’:

  • (continuação)
    • o ORM retornará uma entidade [Elève(id, nom, prénom, classe, notes)]. Devido ao [lazy=’select’], as propriedades [classe, notes] vinculadas a chaves estrangeiras do banco de dados não terão sido calculadas;
    • agora, se eu exibir a cadeia jSON desse aluno, sabemos que será a cadeia jSON do dicionário [asdict] da entidade. Se as propriedades [classe] e [notes] estiverem presentes, [sqlalchemy] será obrigada a fazer consultas a SQL para calcular seus valores. Isso é dispendioso. Se for possível evitar essas consultas, é preferível;
    • aqui, excluímos todas as propriedades vinculadas 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 de configuração do aplicativo;
  • linha 109: esse dicionário é devolvido;

O arquivo [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

    # retornamos a configuração
    return config
  • linha 1: a função [configure] recebe o dicionário da configuração global do aplicativo;
  • 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 é retornada;

19.6.5. A camada [dao] - 1

Image

É preciso entender aqui que a camada [dao] [3] se comunica com acamada ORM [sqlalchemy] [4], configurada conforme descrito no parágrafo anterior. Das três camadas [ui, métier, dao] do aplicativo |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 do banco 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, quanto da interface [InterfaceDao] do projeto |troiscouches v01|;
  • linhas 8-11: adiciona-se o método [init_database] aos métodos herdados de [InterfaceDao]. Sua função será inicializar o banco de dados com os dados do dicionário [data], que lhe são passados como parâmetro na linha 10;

Vale lembrar 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

    # pesquisar 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):
        # banco 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. Ele recebe como parâmetro o dicionário de configuração do aplicativo;
  • linha 15: armazena-se a configuração [sqlalchemy];
  • linha 16: armazena-se a sessão [sqlalchemy] por meio da qual se irá manipular o banco de dados;
  • linha 18: o método [init_database] inicializa o banco 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 gerados
    return data
  • linha 34: o dicionário que será passado para o método [init_database]. Esse dicionário é composto pelas seguintes chaves (linha 32):
    • [élèves]: a lista de alunos;
    • [classes]: a lista de turmas;
    • [matières]: a lista de 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 do banco de dados
        database = self.database
        engine = database['engine']
        metadata = database['metadata']
        tables = database['tables']

        try:
            # exclusã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:
            # cancelamento da sessão
            if session:
                session.rollback()
            # a exceção é reenviada
            raise MyException(23, f"{erreur}")
  • linhas 3-6: recuperamos informações da configuração do banco de dados;
  • linhas 9-14: vimos que a configuração [sqlalchemy] havia mapeado quatro entidades em quatro tabelas [élèves, matières, classes, notes]. Começamos por excluir essas tabelas, caso existam;
  • linhas 16-17: recriamos as quatro tabelas que acabamos de excluir;
  • linhas 22-25: colocamos todas as classes na sessão;
  • linhas 27-30: colocamos todas as disciplinas na sessão;
  • linhas 32-35: colocamos todos os alunos na sessão;
  • linhas 37-40: inserimos todas as notas na turma;
  • para fazer essas adições, seguimos uma ordem. Começamos pelas entidades que não tinham relações com outras entidades e terminamos pelas que tinham. Assim, quando adicionamos os alunos à sessão, as turmas às quais eles estão vinculados já estão na sessão;
  • linha 43: a sessão [sqlalchemy] foi validada. Após essa operação, temos certeza de que todos os dados da sessão foram sincronizados com o banco de dados. Em outras palavras, eles foram inseridos nas tabelas. Isso 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 do banco de dados

Image

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


# aguardando 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()

# configurando o aplicativo
import config
config = config.configure({'sgbd': sgbd})

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

# recuperando os dados a serem inseridos no banco de dados
import data
data = data.configure()

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

# ----------- principal
try:
    # criação e inicialização das tabelas do banco de dados
    dao.init_database(data)
except MyException as ex:
    # exibição do erro
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # liberação dos recursos utilizados pelo aplicativo
    import shutdown
    shutdown.execute(config)
# fim
print("Travail terminé...")
  • linhas 1-11: o script espera um parâmetro [mysql] ou [pgres], dependendo se se deseja inicializar um banco de dados MySQL ou PostgreSQL;
  • linhas 13-15: o aplicativo está configurado para o SGBD passado como parâmetro;
  • linhas 20-22: recuperam-se os dados a serem inseridos no banco de dados;
  • linha 25: a camada [dao] já foi instanciada e está acessível na configuração do aplicativo;
  • linha 30: o banco de dados é inicializado;
  • linhas 34-37: haja ou não erro, liberamos os recursos do aplicativo por meio do módulo [shutdown];

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


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

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

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

Image

Os resultados da execução dessa 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

Na execução, os resultados no [pgAdmin] são os seguintes:

Image

Image

Image

Vale destacar 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é o momento, mostramos apenas a implementação do método [init_database]. Apresentamos agora a implementação dos demais 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):
        # banco 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 — uma exceção é lançada caso ele não exista
        # permite-se que ela volte
        élève = self.get_élève_by_id(élève_id)
        # recuperam-se as notas dele (carregamento diferido)
        notes = élève.notes
        # retorna-se um dicionário
        return {"élève": élève, "notes": notes}

    # um aluno identificado pelo seu número
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # procuramos o aluno
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # encontramos?
        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 nome
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # estamos procurando o aluno
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # já foi encontrado?
        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 número
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # procurando a turma
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # já foi encontrada?
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # uma turma identificada pelo nome
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # estamos procurando a classe
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # foi encontrada?
        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úmero
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # procurando 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 nome
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # estamos procurando a 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 retornar a lista das turmas da escola. Na linha 20, utilizamos uma consulta já vista anteriormente;
  • 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 retornar um aluno identificado por seu número. Ele lança uma exceção caso o aluno não exista;
    • linha 54: utilizamos uma consulta filtrada. Obtemos uma lista vazia ou com um elemento;
    • linha 57: se a lista recuperada não estiver vazia, retorna-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 retornar as notas de um aluno identificado por seu número:
    • 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: retorna-se um dicionário;
  • linhas 61-109: uma série de métodos semelhantes que permitem:
    • localizar um aluno pelo nome, linhas 61-69;
    • localizar 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] recebeu esse nome porque tem como objetivo destacar as consultas feitas implicitamente pelo [sqlalchemy] para recuperar informações pertencentes a várias tabelas. Essas consultas, ocultas ao programador, são feitas 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="selecionar"),
        'disciplina': relationship(Disciplina, backref="notas", lazy="select")
    })

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

  • linha 5: quando a propriedade [élève] de uma entidade [Note] for solicitada pela primeira vez, ela será buscada na tabela [élèves] por meio de uma consulta SQL. Enquanto essa propriedade não for solicitada, ela permanece indefinida (carregamento diferido). Uma vez obtida, seu valor permanece na memória da ORM. Quando for referenciada pela segunda vez, a ORM fornecerá imediatamente seu valor sem passar por uma nova consulta SQL. Tudo isso é transparente para o desenvolvedor;
  • o mesmo vale para a propriedade inversa [Elève.notes] (backref), linha 5;
  • o mesmo vale para a propriedade [Note.matière] e sua propriedade inversa [Matière.notes] (backref), linha 6;

O script [main_joined_queries] é o seguinte:


# espera-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()

# configurando o aplicativo
import config
config = config.configure({"sgbd": sgbd})

# o syspath está configurado — é possível realizar 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')}")
    # 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 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 vale 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)

    # as notas em uma 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 vale 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:
    # exibe-se 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:
    # os recursos são liberados
    import shutdown
    shutdown.execute(config)

Os comentários são suficientes para entender 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 entender esses resultados, é preciso lembrar que excluímos certas 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, ao escrever [print(f"élève={élève}")] na linha 26 do código, a linha 1 acima indica que as propriedades ['_sa_instance_state', 'notes', 'classe'] não serão exibidas. É isso que vemos na linha 3 dos resultados. Todas as outras propriedades são exibidas. Assim, ainda na linha 3, descobrimos uma nova propriedade [classe_id] que inicialmente não existia na entidade [Elève]. Essa 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 estar ciente disso, principalmente porque elas 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 excluirmos as propriedades [notes, élève] da entidade [Elève], a operação [print(f"élève={élève}")] irá exibi-las e, portanto, como acabou de ser explicado, gerar consultas SQL implícitas (carregamento diferido) para recuperar os valores dessas propriedades. Se, como neste caso, for exibida uma lista de alunos, as operações implícitas SQL são realizadas para cada aluno. Isso pode ser, por um lado, desnecessário e, por outro, certamente oneroso 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 mesmo já utilizado no aplicativo |troiscouches v01]. Na época, ele se chamava [main]. Trata-se de um aplicativo de console que permite obter certos indicadores sobre as notas de um aluno: [moyenne pondérée, min, max, liste]. Ele se insere na seguinte arquitetura:

Image

Nessa arquitetura em camadas, apenas a camada [dao] foi alterada entre o aplicativo |troiscouches v01| e este. Como a nova camada [dao] respeita a interface [InterfaceDao] da antiga camada [dao], as camadas [ui, métier] não precisam ser alteradas. Portanto, é possível continuar utilizando as camadas definidas no aplicativo |troiscouches v01|.

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


# aguarda 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()

# configurando o aplicativo
import config
config = config.configure({'sgbd': sgbd})

# o syspath está configurado — é possível realizar 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:
    # exibindo 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:
    # os recursos são liberados
    import shutdown
    shutdown.execute(config)
  • linha 20: obtém-se uma referência à camada [ui] na configuração do aplicativo;
  • linha 24: inicia-se a interação com o usuário por meio do único método da camada [ui];

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

Image

Aqui está um exemplo de execução com essa 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