📚 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:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Validade do Schema: Confirme se o arquivo prisma/schema.prisma foi criado com sucesso no repositório tecloja-backend.
  2. Validação de sintaxe: Execute npx prisma validate no 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.


Voltar para o Sumário