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.
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).
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 enumTipoMovimentacao? Defesa em profundidade: o enum garante isso no nível da aplicação, mas oCHECKgarante 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.
Ação: TipoMovimentacao é um enum simples. MovimentacaoEstoque segue exatamente o padrão de record + @Table já usado em Produto/Usuario em _01.
model/TipoMovimentacao.javapackage br.com.aula.gestaodeestoques.model;
public enum TipoMovimentacao {
ENTRADA, SAIDA
}
model/MovimentacaoEstoque.javapackage 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
) {}
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é umrecordimutável — adicionar um campo novo quebra a compilação em TODO lugar que chamanew Produto(...)com a lista antiga de argumentos. Em_02isso afetouDataSeeder,ProdutoMapper(nos 3 métodos:toDTO,toEntity,toFormDTO) eProdutoServiceImpl.update(). O compilador Java aponta exatamente esses pontos — é seguro confiar nos erros de compilação como checklist.
Ação: MovimentacaoEstoqueRepository é novo. ProdutoRepository ganha duas queries novas para RF09 (busca) e RF11 (alerta).
repository/MovimentacaoEstoqueRepository.javapackage 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();
}
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
@Queryexplí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@Queryexplícita evita ambiguidade e deixa o SQL real visível, o que já era a preferência do projeto em_01(verUsuarioRepository.findPapeisByUsuarioIdem_01/modulo01.md).
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.