💎 Módulo 01: Nova Modelagem e Persistência

Com os 5 artefatos de design prontos (Módulo 00), começamos a implementação pela camada de dados: o schema do banco, as entidades Java e os repositórios. Este módulo assume que você já copiou gestaodeestoques_01/gestaodeestoques/ como ponto de partida — só o que muda em relação a _01 é coberto aqui.


### Aula 1.1: Alterando o Schema

Ação: Produto ganha a coluna estoque_minimo. A tabela nova movimentacao_estoque guarda cada entrada/saída — ela é o histórico (RF10), não existe uma tabela “histórico” separada (ver Módulo 00).

Código: src/main/resources/schema.sql (trechos alterados)

DROP TABLE IF EXISTS movimentacao_estoque, produto, categoria, fornecedor, usuario_papel, usuario, papel;

-- ... categoria, fornecedor, usuario, papel, usuario_papel inalterados (ver _01) ...

CREATE TABLE produto (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(255) NOT NULL,
    quantidade INT NOT NULL,
    preco DECIMAL(10, 2) NOT NULL,
    estoque_minimo INT NOT NULL DEFAULT 10,
    categoria_id INT,
    fornecedor_id INT,
    FOREIGN KEY (categoria_id) REFERENCES categoria(id),
    FOREIGN KEY (fornecedor_id) REFERENCES fornecedor(id)
);

-- Nova em _02: histórico de entradas/saídas. O histórico É a própria lista de
-- movimentações — não existe uma tabela "historico" separada (RF10/RNF05).
CREATE TABLE movimentacao_estoque (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    produto_id INT NOT NULL,
    tipo VARCHAR(10) NOT NULL CHECK (tipo IN ('ENTRADA', 'SAIDA')),
    quantidade INT NOT NULL,
    data_hora TIMESTAMP NOT NULL,
    usuario_id BIGINT NOT NULL,
    FOREIGN KEY (produto_id) REFERENCES produto(id),
    FOREIGN KEY (usuario_id) REFERENCES usuario(id)
);

💡 Por que CHECK (tipo IN ('ENTRADA', 'SAIDA')) no banco, se o Java já tem o enum TipoMovimentacao? Defesa em profundidade: o enum garante isso no nível da aplicação, mas o CHECK garante isso também para qualquer outro cliente que grave direto no banco (um script de migração, uma ferramenta de BI, um bug futuro que contorne a camada de serviço). Nunca custa nada e evita dado inconsistente na fonte da verdade.

schema-prod.sql recebe a mesma alteração, na sintaxe idempotente do PostgreSQL (CREATE TABLE IF NOT EXISTS, sem DROP TABLE) já usada em _01.


### Aula 1.2: Novas Entidades

Ação: TipoMovimentacao é um enum simples. MovimentacaoEstoque segue exatamente o padrão de record + @Table já usado em Produto/Usuario em _01.

Código: model/TipoMovimentacao.java

package br.com.aula.gestaodeestoques.model;

public enum TipoMovimentacao {
    ENTRADA, SAIDA
}

Código: model/MovimentacaoEstoque.java

package br.com.aula.gestaodeestoques.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;

@Table("MOVIMENTACAO_ESTOQUE")
public record MovimentacaoEstoque(
    @Id Long id,
    Integer produtoId,
    TipoMovimentacao tipo,
    int quantidade,
    LocalDateTime dataHora,
    Long usuarioId
) {}

Código: model/Produto.java (campo novo)

@Table("PRODUTO")
public record Produto(
    @Id Integer id,
    String nome,
    int quantidade,
    BigDecimal preco,
    int estoqueMinimo,
    Integer categoriaId,
    Integer fornecedorId
) {}

⚠️ Gotcha de quem for implementar isso na mão: Produto é um record imutável — adicionar um campo novo quebra a compilação em TODO lugar que chama new Produto(...) com a lista antiga de argumentos. Em _02 isso afetou DataSeeder, ProdutoMapper (nos 3 métodos: toDTO, toEntity, toFormDTO) e ProdutoServiceImpl.update(). O compilador Java aponta exatamente esses pontos — é seguro confiar nos erros de compilação como checklist.


### Aula 1.3: Repositórios

Ação: MovimentacaoEstoqueRepository é novo. ProdutoRepository ganha duas queries novas para RF09 (busca) e RF11 (alerta).

Código: repository/MovimentacaoEstoqueRepository.java

package br.com.aula.gestaodeestoques.repository;
import br.com.aula.gestaodeestoques.model.MovimentacaoEstoque;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MovimentacaoEstoqueRepository extends CrudRepository<MovimentacaoEstoque, Long> {

    @Query("SELECT * FROM movimentacao_estoque WHERE produto_id = :produtoId ORDER BY data_hora DESC")
    List<MovimentacaoEstoque> findByProdutoIdOrderByDataHoraDesc(@Param("produtoId") Integer produtoId);

    @Query("SELECT * FROM movimentacao_estoque ORDER BY data_hora DESC")
    List<MovimentacaoEstoque> findAllOrderByDataHoraDesc();
}

Código: repository/ProdutoRepository.java (queries novas)

@Repository
public interface ProdutoRepository extends CrudRepository<Produto, Integer> {

    // RF09: pesquisar produtos por nome (case-insensitive, substring).
    @Query("SELECT * FROM produto WHERE UPPER(nome) LIKE UPPER(CONCAT('%', :nome, '%'))")
    List<Produto> findByNomeContainingIgnoreCase(@Param("nome") String nome);

    // RF11: produtos cuja quantidade atual já atingiu (ou passou) o estoque mínimo.
    @Query("SELECT * FROM produto WHERE quantidade <= estoque_minimo")
    List<Produto> findComEstoqueBaixo();
}

💡 Por que @Query explícita em vez de Query Methods (findByNomeContaining)? O Spring Data JDBC (diferente do JPA) tem suporte mais limitado a query derivation por nome de método para strings — usar @Query explícita evita ambiguidade e deixa o SQL real visível, o que já era a preferência do projeto em _01 (ver UsuarioRepository.findPapeisByUsuarioId em _01/modulo01.md).


Conclusão do Módulo 01

A camada de dados está pronta: schema, entidades e repositórios cobrindo MovimentacaoEstoque e as queries novas de Produto. No próximo módulo, construímos a regra de negócio que orquestra tudo isso — o serviço que registra uma movimentação, valida saldo e atualiza o produto na mesma transação.