📚 Módulo 04: Backend - Serviços e Transações Assíncronas
Em Engenharia de Software, os controladores (routers) devem ser o mais simples possível, encarregando-se apenas da recepção e do retorno HTTP. O cérebro da nossa aplicação, contendo as regras e a orquestração do negócio, reside na Camada de Serviço (Service Layer).
Neste módulo, implementaremos as regras de negócio para faturamento de pedidos da TecLoja 02 de forma totalmente assíncrona, focando no princípio ACID de consistência transacional do estoque usando sessões do SQLAlchemy.
🏛️ 1. O Padrão Service com Sessão Assíncrona
Diferente do ecossistema Spring Boot, que gerencia dependências e transações por anotações implícitas (@Service, @Transactional), no ecossistema Python profissional nós fazemos o fluxo de dados explícito. Passamos a sessão de banco de dados (AsyncSession) no construtor de nosso serviço, o que simplifica drasticamente a escrita de testes unitários mockados.
Criaremos a estrutura de serviços no pacote app/services/.
📦 2. Serviço de Produtos
Este serviço encapsula todas as leituras e escritas no catálogo. As consultas de busca por categoria ou busca livre por nome usam o padrão SQL do SQLAlchemy 2.0.
Crie o arquivo em app/services/produto_service.py:
from typing import List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.produto import Categoria, Produto
from app.schemas.produto_schema import ProdutoFormSchema, ProdutoSchema
from app.schemas.mappers import Mapper
from app.exceptions import ResourceNotFoundException
class ProdutoService:
def __init__(self, session: AsyncSession):
self.session = session
async def listar_todos(self) -> List[ProdutoSchema]:
query = select(Produto)
result = await self.session.execute(query)
produtos = result.scalars().all()
return [Mapper.produto_to_schema(p) for p in produtos]
async def buscar_por_id(self, id: int) -> ProdutoSchema:
p = await self.session.get(Produto, id)
if not p:
raise ResourceNotFoundException(f"Produto não encontrado com ID {id}")
return Mapper.produto_to_schema(p)
async def pesquisar_por_nome(self, busca: str) -> List[ProdutoSchema]:
query = select(Produto).where(Produto.nome.ilike(f"%{busca}%"))
result = await self.session.execute(query)
produtos = result.scalars().all()
return [Mapper.produto_to_schema(p) for p in produtos]
async def criar(self, form: ProdutoFormSchema) -> ProdutoSchema:
cat = await self.session.get(Categoria, form.categoria_id)
if not cat:
raise ResourceNotFoundException(f"Categoria {form.categoria_id} não encontrada.")
p = Produto(
nome=form.nome,
descricao=form.descricao,
preco=form.preco,
estoque=form.estoque,
categoria=cat
)
self.session.add(p)
await self.session.flush() # Gera o ID do banco antes do commit
return Mapper.produto_to_schema(p)
async def atualizar(self, id: int, form: ProdutoFormSchema) -> ProdutoSchema:
p = await self.session.get(Produto, id)
if not p:
raise ResourceNotFoundException(f"Produto {id} não encontrado.")
cat = await self.session.get(Categoria, form.categoria_id)
if not cat:
raise ResourceNotFoundException(f"Categoria {form.categoria_id} não encontrada.")
p.nome = form.nome
p.descricao = form.descricao
p.preco = form.preco
p.estoque = form.estoque
p.categoria = cat
await self.session.flush()
return Mapper.produto_to_schema(p)
async def deletar(self, id: int) -> None:
p = await self.session.get(Produto, id)
if not p:
raise ResourceNotFoundException(f"Produto {id} não encontrado.")
await self.session.delete(p)
await self.session.flush()
🛒 3. Serviço de Pedidos e Baixa Transacional de Estoque
O faturamento de pedidos (Checkout) é a regra de negócio central da nossa aplicação. O fluxo de validações deve garantir:
- Validade do Cliente.
- Existência física de todos os itens do carrinho na base de dados.
- Disponibilidade de Estoque: se o cliente solicitar 3 unidades e houver apenas 2 no estoque, toda a operação de venda deve ser imediatamente abortada.
- Cópia histórica do preço unitário do eletrônico no ato da compra.
- Gravação segura em cascata do
Pedidoe seus respectivosItemPedido.
🧠 Como funciona a transação com async with session.begin()?
Em Engenharia de Software, a consistência dos dados é garantida por transações ACID (Atômicas, Consistentes, Isoladas e Duráveis).
Ao enveloparmos o método com async with self.session.begin():, abrimos um escopo transacional ativo com o Neon PostgreSQL. Se percorrermos o carrinho debitando o estoque de 3 produtos com sucesso e, no 4º produto, o estoque estiver esgotado, a exceção BusinessException será disparada.
O bloco de contexto capturará a exceção e executará automaticamente um Rollback total da transação no banco de dados. Os 3 produtos anteriores retornarão aos seus estoques originais e nenhuma linha de pedido será persistida fisicamente, prevenindo de forma absoluta estados inconsistentes.
sequenceDiagram
participant Front as Vue 3 (Carrinho)
participant API as FastAPI Router
participant Service as PedidoService
participant DB as PostgreSQL (Neon)
Front->>API: POST /api/pedidos (DTO)
API->>Service: realizar_pedido()
Service->>DB: BEGIN TRANSACTION
loop Para cada Item
Service->>DB: Busca Produto
alt Estoque Insuficiente
Service-->>API: BusinessException
Service->>DB: ROLLBACK TRANSACTION
API-->>Front: 400 Bad Request
else Estoque Ok
Service->>Service: Debita Estoque
end
end
Service->>DB: FLUSH / COMMIT TRANSACTION
Service-->>API: PedidoSchema (DTO)
API-->>Front: 201 Created
Crie o arquivo em app/services/pedido_service.py:
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.pedido import Cliente, Pedido, ItemPedido
from app.models.produto import Produto
from app.schemas.pedido_schema import PedidoFormSchema, PedidoSchema
from app.schemas.mappers import Mapper
from app.exceptions import ResourceNotFoundException, BusinessException
class PedidoService:
def __init__(self, session: AsyncSession):
self.session = session
async def realizar_pedido(self, form: PedidoFormSchema) -> PedidoSchema:
# async with session.begin() inicia a transação ativa no banco.
# Qualquer falha ou exceção dentro do escopo dispara ROLLBACK automático!
async with self.session.begin():
# 1. Validar existência do Cliente
cliente = await self.session.get(Cliente, form.cliente_id)
if not cliente:
raise ResourceNotFoundException(f"Cliente {form.cliente_id} não cadastrado.")
# 2. Instanciar novo Pedido
pedido = Pedido(
cliente=cliente,
status="PAGO",
data_pedido=datetime.utcnow(),
itens=[]
)
self.session.add(pedido)
# 3. Processar itens, validar estoque e debitar saldo físico
for item_form in form.itens:
produto = await self.session.get(Produto, item_form.produto_id)
if not produto:
raise ResourceNotFoundException(f"Produto {item_form.produto_id} não encontrado no catálogo.")
# Regra de Ouro: Checar estoque físico
if produto.estoque < item_form.quantidade:
raise BusinessException(
f"Estoque insuficiente para o eletrônico '{produto.nome}'. "
f"Solicitado: {item_form.quantidade}, Disponível: {produto.estoque}."
)
# Debitar estoque físico do produto
produto.estoque -= item_form.quantidade
# Instanciar registro de ItemPedido com cópia histórica de preço
item_pedido = ItemPedido(
pedido=pedido,
produto=produto,
quantidade=item_form.quantidade,
preco_unitario=produto.preco # Copia preço ATUAL (histórico de venda)
)
pedido.itens.append(item_pedido)
# Grava fisicamente na base e executa o commit implícito no encerramento do bloco 'begin'
await self.session.flush()
# Recarrega relações necessárias para o mapeamento DTO
return Mapper.pedido_to_schema(pedido)
✅ Pré-Requisitos deste Módulo
Antes de avançar para as regras de negócio assíncronas, certifique-se de que:
- Os Schemas Pydantic (DTOs) e as Exceções customizadas estão criados e testados no Módulo 03.
- A pasta
app/services/foi estruturada em seu repositório backend.
🤔 Por que fizemos assim?
- Por que injetar
AsyncSessionno construtor do serviço? Diferente de frameworks que utilizam injeção implícita global, passar a sessão ativamente no construtor (__init__) deixa o código desacoplado e transparente. Isso simplifica testes de unidade, permitindo passar um mock da sessão SQL de forma direta sem necessitar de inicialização de servidores locais de banco de dados. - Por que abrir a transação de forma explícita com
async with session.begin()? Transações financeiras de checkout exigem conformidade estrita com a propriedade de Atomicidade (ou tudo funciona ou nada é gravado). O bloco de contexto do SQLAlchemy 2.0 cuida da abertura e fechamento da transação automaticamente: caso o estoque acabe na metade do processamento do carrinho, o disparo da exceção de negócio aborta o escopo de execução e executa o Rollback de todas as operações anteriores automaticamente no banco de dados, protegendo o estoque físico de inconsistências. - Por que gravar o preço unitário histórico no
ItemPedido? O preço de catálogo de um produto flutua constantemente. Se o preço unitário histórico não for copiado e gravado fixamente na tabela associativa de vendas no ato do checkout, futuras readequações ou promoções de catálogo iriam distorcer e adulterar o faturamento histórico de vendas da empresa.
🔍 Checkpoint
- Estrutura da Camada Service: Verifique se as classes de regras de negócio foram criadas nos caminhos físicos corretos:
tecloja-backend/app/services/produto_service.pytecloja-backend/app/services/pedido_service.py
- Sintaxe Transacional: Confirme que o método
realizar_pedidono serviço de pedidos utiliza o escopoasync with self.session.begin():para proteger o banco.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
sqlalchemy.exc.InterfaceError: conn.send_parameter() is not supported |
Tentativa de utilizar tipos de dados incompatíveis ou não mapeados na query (ex: passar objetos Pydantic em vez de valores puros nas cláusulas where). |
Certifique-se de extrair os valores primitivos dos Schemas (ex: form.categoria_id) antes de passá-los aos filtros e consultas do SQLAlchemy. |
O banco de dados grava dados mesmo após o disparo de uma BusinessException |
O escopo transacional foi aberto incorretamente ou o rollback automático falhou por commits explícitos intermediários executados manualmente. | Remova chamadas explícitas de await session.commit() de dentro do escopo async with session.begin():. A persistência automática e o commit são delegados inteiramente ao encerramento do escopo do bloco de contexto. |
🏁 Conclusão
Sensacional! Toda a lógica de negócio transacional assíncrona está pronta, garantindo que o catálogo de produtos e as vendas operem de forma impecável, blindados contra anomalias e concorrência incoerente.
No Módulo 05, criaremos os roteadores REST do FastAPI, habilitaremos o controle de origens compartilhadas (CORS Middleware) para aceitar conexões vindas do Vue 3 hospedado na Netlify e adicionaremos segurança com autenticação stateless por tokens assinados de JWT.