📚 Módulo 01: Modelagem e Banco de Dados

✅ Pré-Requisitos deste Módulo

Antes de começar, confirme:

java -version   # deve retornar 17.x

Você deve ter concluído o Módulo 00 (projeto gerado pelo Spring Initializr e estrutura de pastas criada). Se ainda não configurou o ambiente, consulte o Guia de Setup.


Neste módulo, daremos início à construção da persistência de dados do nosso e-commerce. Você aprenderá a modelar relacionamentos complexos, a traduzir esse modelo em scripts DDL compatíveis com bancos relacionais corporativos e a mapear essas tabelas em entidades Java utilizando a especificação JPA (Jakarta Persistence).


📊 1. Modelo de Dados Conceitual

Um banco de dados de e-commerce é rico em relacionamentos de cardinalidade variada. Nosso projeto contempla:

Diagrama Entidade-Relacionamento (ER) - Mermaid

erDiagram
    CATEGORIA {
        Long id PK "Auto-incremento"
        String nome "Único, não nulo"
    }

    PRODUTO {
        Long id PK "Auto-incremento"
        String nome "Não nulo"
        String descricao "Opcional"
        BigDecimal preco "Não nulo"
        int estoque "Não nulo"
        boolean ativo "Controle de Soft Delete"
        Long categoria_id FK "Chave estrangeira"
    }

    CLIENTE {
        Long id PK "Auto-incremento"
        String nome "Não nulo"
        String email "Único, não nulo"
        String cpf "Único, não nulo"
    }

    PEDIDO {
        Long id PK "Auto-incremento"
        LocalDateTime data_pedido "Não nulo"
        String status "Pendente, Pago, Cancelado"
        Long cliente_id FK "Chave estrangeira"
    }

    ITEM_PEDIDO {
        Long id PK "Auto-incremento"
        int quantidade "Não nulo"
        BigDecimal preco_unitario "Preço histórico"
        Long pedido_id FK "Chave estrangeira"
        Long produto_id FK "Chave estrangeira"
    }

    USUARIO {
        Long id PK "Auto-incremento"
        String username "Único, não nulo"
        String senha "Criptografada BCrypt"
    }

    PAPEL {
        Long id PK "Auto-incremento"
        String nome "ROLE_USER, ROLE_ADMIN"
    }

    CATEGORIA ||--|{ PRODUTO : "classifica"
    CLIENTE ||--|{ PEDIDO : "realiza"
    PEDIDO ||--|{ ITEM_PEDIDO : "contém"
    PRODUTO ||--|{ ITEM_PEDIDO : "compõe"
    USUARIO }o--o{ PAPEL : "possui (usuario_papel)"

[!IMPORTANT] Conceito de Banco de Dados - Preço Histórico: Repare que a tabela ITEM_PEDIDO armazena o campo preco_unitario. Isso é uma regra crucial de Banco de Dados. Se o preço do Produto mudar no futuro, o valor histórico cobrado no momento da compra do Pedido deve permanecer intacto para fins de auditoria e contabilidade.


🗄️ 2. Script Físico de Criação (schema.sql)

Crie o arquivo schema.sql em src/main/resources/ para gerar a estrutura de tabelas. Esse script serve tanto para o banco em memória H2 (desenvolvimento) quanto para o Neon PostgreSQL (produção).

-- Remover tabelas existentes para garantir um estado limpo em Dev
DROP TABLE IF EXISTS item_pedido;
DROP TABLE IF EXISTS pedido;
DROP TABLE IF EXISTS cliente;
DROP TABLE IF EXISTS produto;
DROP TABLE IF EXISTS categoria;
DROP TABLE IF EXISTS usuario_papel;
DROP TABLE IF EXISTS usuario;
DROP TABLE IF EXISTS papel;

-- Tabelas Auxiliares / Domínio
CREATE TABLE categoria (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nome VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE produto (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nome VARCHAR(255) NOT NULL,
    descricao VARCHAR(500),
    preco DECIMAL(10, 2) NOT NULL,
    estoque INT NOT NULL DEFAULT 0,
    ativo BOOLEAN NOT NULL DEFAULT TRUE,
    categoria_id BIGINT NOT NULL,
    FOREIGN KEY (categoria_id) REFERENCES categoria(id)
);

CREATE TABLE cliente (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nome VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    cpf VARCHAR(14) NOT NULL UNIQUE
);

CREATE TABLE pedido (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    data_pedido TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    status VARCHAR(50) NOT NULL,
    cliente_id BIGINT NOT NULL,
    FOREIGN KEY (cliente_id) REFERENCES cliente(id)
);

CREATE TABLE item_pedido (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    quantidade INT NOT NULL,
    preco_unitario DECIMAL(10, 2) NOT NULL,
    pedido_id BIGINT NOT NULL,
    produto_id BIGINT NOT NULL,
    FOREIGN KEY (pedido_id) REFERENCES pedido(id) ON DELETE CASCADE,
    FOREIGN KEY (produto_id) REFERENCES produto(id)
);

-- Segurança e Controle de Acesso
CREATE TABLE usuario (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    senha VARCHAR(255) NOT NULL
);

CREATE TABLE papel (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nome VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE usuario_papel (
    usuario_id BIGINT NOT NULL,
    papel_id BIGINT NOT NULL,
    PRIMARY KEY (usuario_id, papel_id),
    FOREIGN KEY (usuario_id) REFERENCES usuario(id) ON DELETE CASCADE,
    FOREIGN KEY (papel_id) REFERENCES papel(id)
);

☕ 3. Mapeamento de Entidades JPA (Java 17)

Criaremos agora as classes de persistência em Java no pacote br.com.tecloja.api.model. As anotações informam ao Hibernate como sincronizar os objetos com as tabelas relacionais.

1. Categoria.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;

@Entity
@Table(name = "categoria")
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String nome;

    // Construtores
    public Categoria() {}
    public Categoria(Long id, String nome) {
        this.id = id;
        this.nome = nome;
    }

    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }
}

2. Produto.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "produto")
public class Produto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nome;

    @Column(length = 500)
    private String descricao;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal preco;

    @Column(nullable = false)
    private int estoque;

    @Column(nullable = false)
    private boolean ativo = true;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "categoria_id", nullable = false)
    private Categoria categoria;

    public Produto() {}

    // Getters, Setters e Construtores
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }
    public String getDescricao() { return descricao; }
    public void setDescricao(String descricao) { this.descricao = descricao; }
    public BigDecimal getPreco() { return preco; }
    public void setPreco(BigDecimal preco) { this.preco = preco; }
    public int getEstoque() { return estoque; }
    public void setEstoque(int estoque) { this.estoque = estoque; }
    public boolean isAtivo() { return ativo; }
    public void setAtivo(boolean ativo) { this.ativo = ativo; }
    public Categoria getCategoria() { return categoria; }
    public void setCategoria(Categoria categoria) { this.categoria = categoria; }
}

3. Cliente.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;

@Entity
@Table(name = "cliente")
public class Cliente {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nome;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false, unique = true, length = 14)
    private String cpf;

    public Cliente() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getCpf() { return cpf; }
    public void setCpf(String cpf) { this.cpf = cpf; }
}

4. Pedido.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "pedido")
public class Pedido {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "data_pedido", nullable = false, updatable = false)
    private LocalDateTime dataPedido = LocalDateTime.now();

    @Column(nullable = false)
    private String status; // PENDENTE, PAGO, CANCELADO

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "cliente_id", nullable = false)
    private Cliente cliente;

    // Relacionamento 1:N forte. A exclusão de um Pedido remove em cascata seus itens.
    @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ItemPedido> itens = new ArrayList<>();

    public Pedido() {}

    // Auxiliar para garantir integridade bidirecional
    public void adicionarItem(ItemPedido item) {
        itens.add(item);
        item.setPedido(this);
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public LocalDateTime getDataPedido() { return dataPedido; }
    public void setDataPedido(LocalDateTime dataPedido) { this.dataPedido = dataPedido; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public Cliente getCliente() { return cliente; }
    public void setCliente(Cliente cliente) { this.cliente = cliente; }
    public List<ItemPedido> getItens() { return itens; }
    public void setItens(List<ItemPedido> itens) { this.itens = itens; }
}

5. ItemPedido.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "item_pedido")
public class ItemPedido {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private int quantidade;

    @Column(name = "preco_unitario", nullable = false, precision = 10, scale = 2)
    private BigDecimal precoUnitario;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "pedido_id", nullable = false)
    private Pedido pedido;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "produto_id", nullable = false)
    private Produto produto;

    public ItemPedido() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public int getQuantidade() { return quantidade; }
    public void setQuantidade(int quantidade) { this.quantidade = quantidade; }
    public BigDecimal getPrecoUnitario() { return precoUnitario; }
    public void setPrecoUnitario(BigDecimal precoUnitario) { this.precoUnitario = precoUnitario; }
    public Pedido getPedido() { return pedido; }
    public void setPedido(Pedido pedido) { this.pedido = pedido; }
    public Produto getProduto() { return produto; }
    public void setProduto(Produto produto) { this.produto = produto; }
}

6. Papel.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;

@Entity
@Table(name = "papel")
public class Papel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String nome; // Ex: ROLE_USER ou ROLE_ADMIN

    public Papel() {}
    public Papel(Long id, String nome) {
        this.id = id;
        this.nome = nome;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }
}

7. Usuario.java

package br.com.tecloja.api.model;

import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "usuario")
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String username;

    @Column(nullable = false)
    private String senha;

    // EAGER fetch é aceitável aqui pois papéis são leves e sempre necessários com o usuário
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "usuario_papel",
        joinColumns = @JoinColumn(name = "usuario_id"),
        inverseJoinColumns = @JoinColumn(name = "papel_id")
    )
    private Set<Papel> papeis = new HashSet<>();

    public Usuario() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getSenha() { return senha; }
    public void setSenha(String senha) { this.senha = senha; }
    public Set<Papel> getPapeis() { return papeis; }
    public void setPapeis(Set<Papel> papeis) { this.papeis = papeis; }
}

🤔 Por que FetchType.LAZY em vez de EAGER?

Em Produto.java usamos @ManyToOne(fetch = FetchType.LAZY) para a relação com Categoria. Com LAZY, o Hibernate não carrega a categoria automaticamente junto ao produto — ela só é buscada no banco quando você de fato acessar produto.getCategoria(). Com EAGER, toda busca de produto dispararia um JOIN desnecessário com a tabela de categorias.

Regra prática: Use LAZY como padrão para todas as relações @ManyToOne e @OneToMany. Reserve EAGER apenas quando a entidade associada é sempre necessária — como vemos em Usuario.papeis (FetchType.EAGER), pois o Spring Security precisa dos papéis toda vez que carrega um usuário.

🤔 Por que a ItemPedido tem seu próprio preco_unitario?

O diagrama N:M entre Pedido e Produto é implementado como duas relações 1:N através da entidade ItemPedido. A coluna preco_unitario existe ali para preservar o preço histórico no momento da venda. Se amanhã o preço do produto for alterado no catálogo, os pedidos antigos continuam com o valor correto — fundamental para integridade contábil.

🔍 Checkpoint

Após criar todos os arquivos .java no pacote model, compile o projeto:

./mvnw clean compile

Resultado esperado: BUILD SUCCESS sem erros. Se aparecer cannot find symbol ou package does not exist, verifique se o pacote declarado em cada arquivo corresponde ao caminho de pasta (br.com.tecloja.api.model).

⚠️ Erros Comuns

Sintoma Causa Solução
BUILD FAILURE: cannot find symbol Categoria Arquivo Categoria.java com pacote errado Confirme que a primeira linha é package br.com.tecloja.api.model;
org.hibernate.LazyInitializationException Acesso a relação LAZY fora de sessão JPA Anote o método de serviço com @Transactional ou use JOIN FETCH no repositório
StackOverflowError ao serializar para JSON Relacionamento bidirecional sendo serializado infinitamente Não exponha entidades JPA como resposta da API — use DTOs (Módulo 03)
Table already exists ao iniciar Conflito entre ddl-auto e schema.sql Confirme spring.jpa.hibernate.ddl-auto=none no application.properties

🏁 Conclusão e Próximos Passos

O banco de dados e as entidades JPA foram completamente mapeados! Agora você possui o conhecimento conceitual sobre relacionamentos relacionais e sua representação no ecossistema Java.

No Módulo 02, faremos o setup físico do projeto Backend, configurando o arquivo Maven pom.xml, parametrizando as conexões do H2 e do Neon PostgreSQL e programando interfaces de repositórios dinâmicas e o carregamento automático de dados para as aulas práticas.


Voltar para o Sumário