ORM e SqlAlchemy (Python)

O que é um ORM?

Um ORM (Object-Relational Mapping) é uma técnica de programação que mapeia objetos definidos em uma linguagem de programação orientada a objetos para estruturas de dados em um banco de dados relacional. Isso permite que os desenvolvedores usem objetos e métodos orientados a objetos para interagir com o banco de dados, em vez de escrever consultas SQL diretamente.

O que é o SQLAlchemy?

O SQLAlchemy é uma biblioteca de mapeamento objeto-relacional (ORM) para Python. Ele fornece uma maneira de mapear objetos Python para tabelas em um banco de dados relacional, facilitando o desenvolvimento de aplicativos que lidam com dados de maneira orientada a objetos. SQLAlchemy suporta uma variedade de bancos de dados SQL e oferece uma API flexível e poderosa para criar consultas, gerenciar transações e interagir com o banco de dados de forma programática.

Configuração do SQLAlchemy

Criação do arquivo de variável de ambiente

Crie um arquivo .env na raíz do projeto e preencha de acordo com a descrição abaixo:

#DB connection
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=

Criação do arquivo de conexão com o banco de dados

from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from core.infrastructure.settings.env_handler import settings

"""
The engine variable is an instance of the create_engine class from the SQLAlchemy library,
responsible for managing the connection to the database.
"""
engine = create_engine(
    f"postgresql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"
)

"""
The SessionLocal variable is an instance of the sessionmaker class from the SQLAlchemy library,
responsible for managing the session with the database.
"""
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

"""
The SqlAlchemyBaseEntity variable is an instance of the declarative_base class from the SQLAlchemy library,
responsible for managing the base class for all the models in the application.
"""
SqlAlchemyBaseEntity = declarative_base()


@contextmanager
def get_session():
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

Observação: É necessário adaptar o import para obter os valores das variáveis de ambiente;

Qual a responsabilidade da função get_session?

@contextmanager
def get_session():
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

A razão para usar yield em vez de return nesta função é para aproveitar a funcionalidade dos geradores e dos gerenciadores de contexto em Python.

Se você usasse return em vez de yield, a função retornaria a sessão, mas não teria controle sobre o que acontece com ela depois. Você não teria garantia de que a sessão seria fechada corretamente, o que poderia levar a vazamentos de recursos.

Ao usar yield, você transforma a função get_session em um gerador. Isso permite que você use a função em uma declaração with, que é um tipo de gerenciador de contexto. Os gerenciadores de contexto garantem que os recursos sejam limpos corretamente, mesmo que ocorra um erro.

Quando você usa get_session em uma declaração with, como with get_session() as session:, o Python automaticamente:

  • Chama a função get_session e inicia a sessão do banco de dados.
  • Consome o primeiro item do gerador (a sessão do banco de dados) e o atribui à variável session.
  • Executa o bloco de código dentro do with.
  • Consome o restante do gerador (neste caso, não há mais itens a serem produzidos).
  • Executa o bloco finally, fechando a sessão do banco de dados.
if __name__ == "__main__":
    create_tables()

    with get_session() as session:
        person = Person(name="John", age=30)
        session.add(person)
        session.commit()
        print("Person added successfully!")

Isso garante que a sessão do banco de dados seja sempre fechada corretamente, mesmo que ocorra um erro dentro do bloco with. Isso não seria possível se você usasse return em vez de yield.

Como implementar a lógica de rollback na sessão com o banco de dados em casos de erro?

@contextmanager
def get_session():
    session = SessionLocal()
    try:
        yield session
    except IntegrityError as e:
        session.rollback()
        raise e
    finally:
        session.close()

O IntegrityError do SQLAlchemy é lançado quando ocorre uma violação de integridade do banco de dados durante a execução de uma operação. Isso pode acontecer em várias situações, como:

  • Violação de chave primária: Tentar inserir uma linha com um valor de chave primária que já existe na tabela.
  • Violação de chave estrangeira: Tentar inserir ou atualizar uma linha com um valor de chave estrangeira que não existe na tabela referenciada.
  • Violação de restrição única (unique constraint): Tentar inserir ou atualizar uma linha com um valor que viola uma restrição única na tabela.
  • Violação de restrição de valor nulo (NOT NULL constraint): Tentar inserir ou atualizar uma linha com um valor nulo em uma coluna que não permite valores nulos.
  • Outras restrições de integridade: Qualquer outra regra de integridade definida no banco de dados, como restrições de verificação (CHECK constraints).

Como explicado anteriormente, a função get_session gerencia o ciclo de vida da sessão com o banco de dados. Isso inclui garantir que a sessão seja aberta, usada e fechada corretamente. Além disso, ela agora lida com erros que podem ocorrer durante o uso da sessão. Em casos de erro, como um IntegrityError, por exemplo, ocorre um rollback. Isso é fundamental para garantir a integridade dos dados e evitar que operações parciais sejam confirmadas no banco de dados em situações inesperadas.

Interessante seu artigo, bem elucidativo. Eu normalmente por usar o context_manager gerencio a sessão usando o with ao invés de por o finally na sessão pois ele já implementa a função mágica __exit__ do context manager do with para fechar a sessão.

Tem alguma razão de querer implementar o autocommit e o autoflush no session maker?

Mas, parabéns pela iniciativa.

Por exemplo, ao habilitar o autocommit como True, não é necessário chamar session.commit() para confirmar as operações no banco de dados, pois isso será feito automaticamente. O mesmo vale para o autoflush quando definido como true.
sim, mas, elas já nao vem setadas assim por default? eu normalmente faco o contrario eu so explicito a config quando quero que seja True o autocommit.
Realmente, passar esses parâmetros como falso é redundante. Boa observação, acabou passando batido.

Vou falar uma coisa que poucos falam com base em experiência de mercado:

SQLAlquemy é extremamente lento quando precisa fazer pesquisas muito pessadas com muitos joins. Na verdade, a maioria das ORM é. Isso é simplesmente porque eles geram uma camada a mais de abstração.

ORM é execelente para update, delete e insert em bancos que de fato usam arquiterura relacional. Mas lembre-se, ser SQL, não quer dizer que fizeram corretamente relaconal.

Exemplo de select até 4x mais rápido que usando ORM:

cursor.execute(f"""
  SELECT autor.aaa, autor.bbb, autor.ccc, xxxx.ddd AS nome_feed, sss.rrrr, ssss.ttttt,
   COUNT(*) AS total, CURRENT_TIMESTAMP()
              FROM db.sssss
              INNER JOIN db.sssss ON ssss.id = xxxx.id_feed
              LEFT JOIN db.autor ON autor.id = feed_evento.id_autor
              INNER JOIN db.sss ON sss.codigo_feed = sss.codigo
              INNER JOIN db.xxx ON xxxxx.codigo = tttttt.codigo_mantenedor
              LEFT JOIN db.yyyyy ON yyyy.id_feed = yyyyy.id
              LEFT JOIN db.tttt ON tttt.id = zzz.id_escopo_coleta
              GROUP BY autor.xxxxxx
              ORDER BY total DESC""")
rs = cursor.fetchall()

Você pode instâciar a conexão de forma que retorne um diciońario com a chave sendo o nome da coluna....

Isso curso nenhum fala, mas vale a pena a reflexão...

Ótima observação! Você fez essa mesma query com o SqlAlchemy na sua base e conseguiu levantar algumas métricas para comparar? Seria bem interessante só pra termos uma noção.