📚 Módulo 01: Modelagem ERD e Banco de Dados com Prisma ORM
Neste módulo, estudaremos a modelagem do banco de dados relacional da TecLoja 03. Desenharemos o diagrama físico de Entidade-Relacionamento (ERD) em Mermaid e programaremos o mapeamento físico e lógico do banco utilizando a linguagem declarativa do Prisma Schema (schema.prisma), cobrindo chaves primárias, estrangeiras e tabelas associativas complexas de N:M.
🗺️ 1. Diagrama Físico de Entidade-Relacionamento (ERD)
O diagrama abaixo detalha a estrutura relacional do nosso banco de dados no PostgreSQL:
erDiagram
CATEGORIA ||--o{ PRODUTO : "contém"
CLIENTE ||--o{ PEDIDO : "realiza"
PEDIDO ||--o{ ITEM_PEDIDO : "possui"
PRODUTO ||--o{ ITEM_PEDIDO : "incluído_em"
USUARIO }|--|| PAPEL : "possui"
CATEGORIA {
int id PK
string nome
}
PRODUTO {
int id PK
string nome
string descricao
decimal preco
int estoque
int categoriaId FK
}
CLIENTE {
int id PK
string nome
string email
string cpf
}
PEDIDO {
int id PK
datetime dataCriacao
string status
decimal valorTotal
int clienteId FK
}
ITEM_PEDIDO {
int id PK
int pedidoId FK
int produtoId FK
int quantidade
decimal precoUnitario
}
USUARIO {
int id PK
string username
string password
int papelId FK
}
PAPEL {
int id PK
string nome
}
💾 2. O Mapeamento Físico com Prisma ORM
Diferente de ORMs tradicionais do Java e Python (como JPA e SQLAlchemy) onde declaramos as tabelas em classes da linguagem de programação, o Prisma centraliza a definição de tabelas, índices e relações em um único arquivo de configuração expressivo chamado schema.prisma.
Abaixo, programaremos o arquivo completo de mapeamento físico da aplicação:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// 1. Tabela Categoria
model Categoria {
id Int @id @default(autoincrement())
nome String @unique @db.VarChar(100)
produtos Produto[] // Relação lógica 1:N reversa
@@map("categorias")
}
// 2. Tabela Produto
model Produto {
id Int @id @default(autoincrement())
nome String @db.VarChar(150)
descricao String? @db.Text
preco Decimal @db.Decimal(10, 2)
estoque Int @default(0)
categoriaId Int
categoria Categoria @relation(fields: [categoriaId], references: [id], onDelete: Cascade)
itens ItemPedido[] // Relação N:M lógica via tabela associativa
@@map("produtos")
}
// 3. Tabela Cliente
model Cliente {
id Int @id @default(autoincrement())
nome String @db.VarChar(100)
email String @unique @db.VarChar(150)
cpf String @unique @db.VarChar(14)
pedidos Pedido[] // Relação lógica 1:N reversa
@@map("clientes")
}
// 4. Tabela Pedido (Entidade Forte)
model Pedido {
id Int @id @default(autoincrement())
dataCriacao DateTime @default(now()) @map("data_criacao")
status String @default("PENDENTE") @db.VarChar(50)
valorTotal Decimal @map("valor_total") @db.Decimal(10, 2)
clienteId Int @map("cliente_id")
cliente Cliente @relation(fields: [clienteId], references: [id], onDelete: Restrict)
itens ItemPedido[] // Relação 1:N com a tabela associativa
@@map("pedidos")
}
// 5. Tabela Associativa N:M (ItemPedido) com atributos históricos extras
model ItemPedido {
id Int @id @default(autoincrement())
pedidoId Int @map("pedido_id")
produtoId Int @map("produto_id")
quantidade Int
precoUnitario Decimal @map("preco_unitario") @db.Decimal(10, 2)
pedido Pedido @relation(fields: [pedidoId], references: [id], onDelete: Cascade)
produto Produto @relation(fields: [produtoId], references: [id], onDelete: Restrict)
@@unique([pedidoId, produtoId]) // Garante que um produto não se repita na mesma compra
@@map("itens_pedido")
}
// 6. Tabela de Papel (Roles)
model Papel {
id Int @id @default(autoincrement())
nome String @unique @db.VarChar(50)
usuarios Usuario[]
@@map("papeis")
}
// 7. Tabela de Usuário para autenticação segura
model Usuario {
id Int @id @default(autoincrement())
username String @unique @db.VarChar(100)
password String @db.VarChar(255)
papelId Int @map("papel_id")
papel Papel @relation(fields: [papelId], references: [id], onDelete: Restrict)
@@map("usuarios")
}
🔎 3. Carga de Dados Relacionais: Eager vs. Lazy Loading no Prisma
No desenvolvimento backend, controlar como as tabelas relacionadas são trazidas da memória do banco de dados é essencial para evitar o clássico gargalo do problema de consulta N+1.
Como o Prisma gerencia isso?
Por padrão, o Prisma adota uma estratégia rígida de carregamento sob demanda (Lazy Loading manual). Ou seja, se consultarmos um Produto direto da API, o Prisma não buscará a sua Categoria correspondente a menos que você solicite explicitamente.
Para fazer a junção das tabelas de forma performática (Eager Loading), o Prisma utiliza o parâmetro include nas suas queries, gerando um único comando JOIN no banco:
// Exemplo: Consultando produtos com suas respectivas categorias anexadas
const produtosComCategoria = await this.prisma.produto.findMany({
include: {
categoria: true, // Eager Loading explícito
},
});
Caso queira obter apenas propriedades específicas da relação para economizar largura de banda:
// Exemplo: Consultando pedidos trazendo apenas o nome do Cliente comprador
const pedidosComClienteNome = await this.prisma.pedido.findMany({
select: {
id: true,
valorTotal: true,
cliente: {
select: {
nome: true,
},
},
},
});
✅ Pré-Requisitos deste Módulo
Antes de passar para as migrações físicas e o seeder de dados no PostgreSQL, certifique-se de que:
- A visão geral da arquitetura cliente-servidor e as pastas físicas foram consolidadas no Módulo 00.
- A extensão de suporte à linguagem Prisma está instalada em seu editor de código (VS Code / Cursor) para suporte à sintaxe e auto-formatação.
🤔 Por que fizemos assim?
- Por que declarar o modelo no arquivo
schema.prismaem vez de usar classes TypeScript (como no TypeORM)? O Prisma adota um modelo de dados declarativo unificado. Em vez de espalhar decorators e lógicas complexas de sincronismo em arquivos de classe TypeScript que podem divergir da estrutura real do banco de dados, o Prisma Centraliza toda a modelagem de tabelas, tipos e chaves num arquivo simples e legível. A partir dele, o Prisma CLI gera de forma automatizada o cliente TypeScript (Prisma Client) com tipagem estática perfeita e auto-complete das consultas. - Por que definir
onDelete: Cascadenas categorias eonDelete: Restrictnos clientes? Regras de integridade referencial previnem inconsistências físicas no banco de dados. MapearonDelete: Cascadena relação Categoria-Produto garante que se uma categoria for excluída, todos os seus produtos associados serão removidos do banco automaticamente, o que condiz com o negócio (um eletrônico não pode existir sem uma categoria pai). Por outro lado,onDelete: Restrictnos Pedidos de Clientes impede que um cliente seja deletado caso ele possua registros históricos de compras finalizadas, blindando os dados financeiros da loja contra exclusões indevidas. - Por que usar a restrição de índice
@@unique([pedidoId, produtoId])no ItemPedido? Em um carrinho de compras de Engenharia de Software, um produto adicionado não deve gerar múltiplos registros idênticos na tabela associativa. A restrição@@uniqueatua no nível físico do banco de dados como uma chave primária composta virtual, impedindo a inserção de tuplas redundantes e forçando o sistema a atualizar de forma atômica a coluna de quantidade do registro existente.
🔍 Checkpoint
- Validade do Schema: Confirme se o arquivo
prisma/schema.prismafoi criado com sucesso no repositóriotecloja-backend. - Validação de sintaxe: Execute
npx prisma validateno diretório do backend para ter certeza de que o compilador do Prisma não encontrou erros de digitação nas chaves, relações ou tipos declarados.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
| TypeScript exibindo erro de que o modelo Categoria ou Produto não existe no Prisma Client | O compilador do Prisma Client local não foi atualizado com base no novo arquivo de mapeamento físico. | Sempre execute npx prisma generate no terminal após efetuar modificações físicas no arquivo schema.prisma para recriar as definições de tipo TypeScript no node_modules. |
| Diferença de arredondamento e perdas de centavos em preços de produtos | Modelagem de moedas ou valores financeiros usando o tipo genérico Float ou Double. |
Sempre utilize o mapeamento @db.Decimal(10, 2) para valores monetários, garantindo a precisão matemática exata e prevenindo bugs de precisão de ponto flutuante do JavaScript no frontend. |
Erro Relation is ambiguous ao compilar relações |
Duas tabelas possuem múltiplos relacionamentos diretos sem que o Prisma saiba associá-los explicitamente. | Nomeie cada relacionamento especificando o decorator @relation("NomeDaRelacao") em ambas as pontas para permitir que o compilador diferencie qual chave estrangeira corresponde a cada propriedade lógica. |
🏁 Conclusão
Com o modelo declarativo do Prisma concluído, temos as estruturas prontas para criar as tabelas físicas no banco de dados Neon PostgreSQL.
No Módulo 02, faremos o setup inicial do servidor NestJS, configuraremos a conexão com o banco na nuvem e executaremos as ferramentas de linha de comando do Prisma Migrate para versionar a evolução física do nosso esquema, além de carregar o banco de dados com dados iniciais através de um script de seeding em TypeScript.