📚 Pré-requisitos Teóricos: Este projeto prático aplica conceitos de modelagem de relacionamentos objeto-relacionais (
@ManyToOne), paginação eficiente com Spring Data JPA (Pageable), Bean Validation avançado e customização dinâmica com CSS Tokens ensinados na Especialização em Backend com Java e Spring Boot.
A Biblioteca de Jogos (v3) expande o modelo de dados para um ecossistema corporativo completo. A aplicação introduz o relacionamento relacional entre as entidades Jogo e Categoria, suporta paginação server-side com ordenação customizada, filtros dinâmicos por categoria, persistência de preferência visual através de Dark/Light Mode com CSS Custom Properties e validações semânticas de campos numéricos e textuais.
O diagrama a seguir descreve o fluxo de dados entre os seletores da interface web, o controlador paginado e o motor de paginação SQL do Spring Data JPA:
A base de código segue o padrão canônico com nomenclatura anti-colisão:
catalogo_projetos/javaweb_jogos_03_paging/
├── assets/ # Galeria de diagramas visuais e arquitetura
│ └── arquitetura_jogos_paging.svg
├── web-spring/ # Aplicação Spring Boot 3 Paginada
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/br/com/bibliotecajogos/
│ │ │ │ ├── controller/ # JogoController com rotas paginadas
│ │ │ │ ├── entity/ # Jogo & Categoria (@ManyToOne)
│ │ │ │ ├── repository/ # JogoRepository & CategoriaRepository
│ │ │ │ └── service/ # JogoService com Pageable
│ │ │ └── resources/
│ │ │ ├── templates/
│ │ │ │ └── index.html # Cards paginados e toggle de tema
│ │ │ ├── static/
│ │ │ │ └── css/style.css
│ │ │ └── application.properties
│ │ └── test/ # Testes de paginação e validação
│ ├── Dockerfile
│ ├── pom.xml
│ ├── mvnw
│ └── mvnw.cmd
└── index.md # Especificação executiva e Padrão Ouro
| ID | Persona | História de Usuário | Prioridade |
|---|---|---|---|
| US-01 | Gamer / Colecionador | Desejo associar cada jogo a uma Categoria específica (RPG, Ação, Estratégia) para melhor catalogação. | Alta |
| US-02 | Usuário da Biblioteca | Desejo paginar os resultados de 6 em 6 jogos para navegar com fluidez sem sobrecarregar a tela. | Alta |
| US-03 | Usuário da Biblioteca | Desejo filtrar os jogos listados por categoria selecionada no cabeçalho da página. | Alta |
| US-04 | Gamer Noturno | Desejo alternar entre temas claro e escuro mantendo minha preferência salva entre sessões no navegador. | Média |
| US-05 | Desenvolvedor Backend | Desejo validar notas (0 a 10) e preços positivos diretamente no modelo antes da persistência. | Alta |
| ID | Nome | Descrição |
|---|---|---|
| RF-01 | Associação de Categoria | Relacionamento @ManyToOne entre Jogo e Categoria com chave estrangeira mapeada. |
| RF-02 | Paginação com Spring Data | Rota GET / recebendo parâmetros page, size e categoriaId, retornando Page<Jogo>. |
| RF-03 | Filtragem Dinâmica | Consulta JPA filtrando por identificador de categoria caso o parâmetro esteja presente. |
| RF-04 | Bean Validation Numérico | Validações @DecimalMin("0.0"), @DecimalMax("10.0") e @Positive na entidade Jogo. |
| RF-05 | Tema Persistente | Alternância de atributo data-theme gravado em localStorage para retenção no cliente. |
| ID | Nome | Descrição |
|---|---|---|
| RNF-01 | Eficiência de Consultas | Execução com cláusulas SQL LIMIT e OFFSET geradas automaticamente pelo Spring Data. |
| RNF-02 | Integridade Referencial | Bloqueio de exclusão em cascata acidental garantindo integridade das categorias. |
| RNF-03 | Tempo de Renderização | Carga de página inferior a 90ms mesmo com catálogo de milhares de registros. |
| RNF-04 | Acessibilidade Visual | Contraste de cores mínimo de 4.5:1 nos cards e botões nos dois modos visuais. |
| RNF-05 | Portabilidade em Nuvem | Parâmetros de conexão JDBC configuráveis via variáveis de ambiente no Render. |
flowchart TD
subgraph Ator
USR["Gamer / Usuário"]
end
subgraph "Biblioteca de Jogos v3 (web-spring)"
UC1["Filtrar Jogos por Categoria"]
UC2["Navegar entre Páginas (Pageable)"]
UC3["Cadastrar Jogo com Categoria e Nota"]
UC4["Alternar Modo Claro / Noturno"]
end
USR --> UC1
USR --> UC2
USR --> UC3
USR --> UC4
| Requisito Funcional | Histórias Atendidas | Requisitos Não-Funcionais | Status de Cobertura |
|---|---|---|---|
| RF-01 (Associação Categoria) | US-01 | RNF-01, RNF-02 | Implementado ✅ |
| RF-02 (Paginação Data JPA) | US-02 | RNF-01, RNF-03 | Implementado ✅ |
| RF-03 (Filtragem Dinâmica) | US-03 | RNF-01, RNF-03 | Implementado ✅ |
| RF-04 (Bean Validation) | US-05 | RNF-02 | Implementado ✅ |
| RF-05 (Tema Persistente) | US-04 | RNF-04 | Implementado ✅ |
Jogo.java)📄 web-spring/src/main/java/br/com/bibliotecajogos/entity/Jogo.java (arquivo real do projeto)
package br.com.bibliotecajogos.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
public class Jogo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "O título é obrigatório")
private String titulo;
private String autor;
private Integer anoPublicacao;
private String genero;
@Column(length = 1000)
private String descricao;
private String plataforma;
private Integer tempoEstimado; // horas
private LocalDate dataLancamento;
@DecimalMin(value = "0.0", message = "A nota mínima é 0")
@DecimalMax(value = "10.0", message = "A nota máxima é 10")
private Double nota;
@Enumerated(EnumType.STRING)
private Dificuldade dificuldade;
private boolean multiplayer;
@Positive(message = "O preço deve ser positivo")
private BigDecimal preco;
private String lojaUrl;
private String urlCapa;
private boolean finalizado;
private LocalDate dataAdicionado = LocalDate.now();
private LocalDate dataFinalizacao;
@NotNull(message = "A categoria é obrigatória")
@ManyToOne
private Categoria categoria;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitulo() { return titulo; }
public void setTitulo(String titulo) { this.titulo = titulo; }
public String getAutor() { return autor; }
public void setAutor(String autor) { this.autor = autor; }
public Integer getAnoPublicacao() { return anoPublicacao; }
public void setAnoPublicacao(Integer anoPublicacao) { this.anoPublicacao = anoPublicacao; }
public String getGenero() { return genero; }
public void setGenero(String genero) { this.genero = genero; }
public String getDescricao() { return descricao; }
public void setDescricao(String descricao) { this.descricao = descricao; }
public String getPlataforma() { return plataforma; }
public void setPlataforma(String plataforma) { this.plataforma = plataforma; }
public Integer getTempoEstimado() { return tempoEstimado; }
public void setTempoEstimado(Integer tempoEstimado) { this.tempoEstimado = tempoEstimado; }
public LocalDate getDataLancamento() { return dataLancamento; }
public void setDataLancamento(LocalDate dataLancamento) { this.dataLancamento = dataLancamento; }
// … (+21 linhas: veja o arquivo completo no projeto)
JogoController.java)📄 web-spring/src/main/java/br/com/bibliotecajogos/controller/JogoController.java (arquivo real do projeto)
package br.com.bibliotecajogos.controller;
import br.com.bibliotecajogos.entity.Categoria;
import br.com.bibliotecajogos.entity.Dificuldade;
import br.com.bibliotecajogos.entity.Jogo;
import br.com.bibliotecajogos.repository.CategoriaRepository;
import br.com.bibliotecajogos.service.JogoService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@Controller
public class JogoController {
@Autowired
private JogoService jogoService;
@Autowired
private CategoriaRepository categoriaRepository;
@GetMapping("/")
public String listar(@RequestParam(defaultValue = "0") int pagina,
@RequestParam(required = false) Long categoriaId,
Model model) {
var jogosPage = jogoService.listar(pagina, categoriaId);
model.addAttribute("jogosPage", jogosPage);
model.addAttribute("currentPage", pagina);
model.addAttribute("categoriaId", categoriaId);
model.addAttribute("categorias", categoriaRepository.findAll());
model.addAttribute("novoJogo", new Jogo());
model.addAttribute("dificuldades", Dificuldade.values());
return "jogos";
}
@PostMapping("/jogos")
public String salvar(@Valid @ModelAttribute("novoJogo") Jogo novoJogo, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("jogosPage", jogoService.listar(0, null));
model.addAttribute("categorias", categoriaRepository.findAll());
model.addAttribute("dificuldades", Dificuldade.values());
return "jogos";
}
jogoService.salvar(novoJogo);
return "redirect:/";
}
@PostMapping("/jogos/{id}/apagar")
public String apagar(@PathVariable Long id) {
jogoService.apagar(id);
return "redirect:/";
}
}
cd catalogo_projetos/javaweb_jogos_03_paging/web-spring
./mvnw spring-boot:run
No Windows (PowerShell):
cd catalogo_projetos\javaweb_jogos_03_paging\web-spring
.\mvnw.cmd spring-boot:run
http://localhost:8080/.localStorage.| Checkpoint | Critério de Aceite | Validação | Status |
|---|---|---|---|
| CP-01 | Relacionamento @ManyToOne funcional com persistência relacional |
Chave estrangeira categoria_id populada |
PASS ✅ |
| CP-02 | Paginação com 6 itens por página controlada por Pageable |
Páginas divididas com ordenação alfabética | PASS ✅ |
| CP-03 | Filtro seletivo por categoria retornando apenas registros associados | Quantidade de registros ajustada dinamicamente | PASS ✅ |
| CP-04 | Bean Validation bloqueando notas fora do intervalo [0.0, 10.0] | Submissão inválida rejeitada com mensagem na tela | PASS ✅ |
| CP-05 | Alternância de Dark Mode funcional e persistida no localStorage |
Token de tema recuperado automaticamente após reload | PASS ✅ |