📚 Pré-requisitos Teóricos: Este projeto prático aplica os conceitos fundamentais de arquitetura MVC, Spring Data JPA, containerização Docker e esteira de banco de dados relacional PostgreSQL ensinados na Especialização em Backend com Java e Spring Boot.

O Guia Completo da Biblioteca de Jogos é um manual técnico aprofundado para engenharia de aplicações web com Spring Boot 3 e Thymeleaf. Ele abrange desde a concepção de modelos relacionais entre Jogo e Categoria, passagem de dados com Spring MVC, orquestração local de contêineres com Docker Compose (PostgreSQL + pgAdmin) e empacotamento multi-stage de imagens de produção.


🏛️ Arquitetura da Solução e Infraestrutura

O diagrama a seguir descreve a integração em três camadas da aplicação Spring Boot e seu ecossistema de contêineres:

Arquitetura da Biblioteca de Jogos com Docker e PostgreSQL

📁 Estrutura de Pastas Padronizada

A base de código segue o padrão canônico com nomenclatura anti-colisão:

catalogo_projetos/javaweb_jogos_guia_crud/
├── assets/                   # Galeria de diagramas visuais e arquitetura
│   └── arquitetura_jogos_guia_crud.svg
├── web-spring/               # Projeto completo Spring Boot com Docker Compose
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/br/com/bibliotecajogos/
│   │   │   │   ├── config/       # DataInitializer (carga inicial de dados)
│   │   │   │   ├── controller/   # JogoController (CRUD completo)
│   │   │   │   ├── entity/       # Jogo & Categoria (JPA)
│   │   │   │   ├── repository/   # JogoRepository & CategoriaRepository
│   │   │   │   └── service/      # JogoService
│   │   │   └── resources/
│   │   │       ├── templates/    # Templates Thymeleaf
│   │   │       └── application.properties
│   │   └── test/
│   ├── docker-compose.yml    # PostgreSQL 16 + pgAdmin 4
│   ├── Dockerfile            # Imagem multi-stage Eclipse Temurin
│   ├── pom.xml
│   ├── mvnw
│   └── mvnw.cmd
└── index.md                  # Guia executivo e tutorial Padrão Ouro

🧑‍💼 Engenharia de Software & Requisitos

1. Histórias de Usuário (US)

ID Persona História de Usuário Prioridade
US-01 Gamer / Usuário Desejo gerenciar minha biblioteca de jogos com operações completas de criação, leitura, edição e exclusão. Alta
US-02 Gamer / Usuário Desejo classificar cada jogo por sua respectiva categoria temática (RPG, Estratégia, Ação). Alta
US-03 Desenvolvedor DevOps Desejo inicializar todo o ambiente de banco de dados e ferramentas de administração via Docker Compose. Alta
US-04 Administrador de Dados Desejo inspecionar tabelas e schemas via interface visual do pgAdmin sem instalar clientes nativos. Média
US-05 Engenheiro de Software Desejo que o banco de desenvolvimento seja populado automaticamente na inicialização (DataInitializer). Média

2. Requisitos Funcionais (RF)

ID Nome Descrição
RF-01 Operações CRUD Completas Endpoints MVC cobrindo fluxos de listar, abrir formulário, salvar e remover jogos.
RF-02 Relacionamento Categoria-Jogo Associação @ManyToOne entre títulos e categorias com integridade referencial.
RF-03 Carga Automática de Dados Classe DataInitializer disparada no startup para criar categorias e jogos padrão.
RF-04 Orquestração Docker Compose Subida simultânea dos contêineres de PostgreSQL e pgAdmin através de rede bridge interna.
RF-05 Feedback de Sucesso e Erro Notificações visuais e mensagens amigáveis em transições de formulário.

3. Requisitos Não-Funcionais (RNF)

ID Nome Descrição
RNF-01 Isolamento de Rede Comunicação entre contêineres via hostname de serviço (db) isolada do host.
RNF-02 Persistência de Volumes Dados do banco PostgreSQL persistidos em volume nomeado evitando perda de dados no teardown.
RNF-03 Arquitetura Multi-Tier Divisão estrita de responsabilidades entre Controller, Service, Repository e Database.
RNF-04 Determinismo no Build Dockerfile com estágio de compilação em cache de dependências do Maven Wrapper.
RNF-05 Escalabilidade de Modelo Esquema relacional estruturado com tipos de dados ANSI SQL adequados.

4. Diagrama de Casos de Uso

flowchart TD
    subgraph Atores
        USR["Gamer / Operador"]
        DEV["Engenheiro DevOps"]
    end

    subgraph "Biblioteca de Jogos (web-spring & infra)"
        UC1["Gerenciar Coleção de Jogos (CRUD)"]
        UC2["Associar Categorias a Jogos"]
        UC3["Subir Infraestrutura com Docker Compose"]
        UC4["Administrar Banco com pgAdmin"]
    end

    USR --> UC1
    USR --> UC2
    DEV --> UC3
    DEV --> UC4

5. Matriz de Rastreabilidade (RTM)

Requisito Funcional Histórias Atendidas Requisitos Não-Funcionais Status de Cobertura
RF-01 (CRUD Completo) US-01 RNF-03, RNF-05 Implementado ✅
RF-02 (Relacionamento Categoria) US-02 RNF-03, RNF-05 Implementado ✅
RF-03 (Carga Automática) US-05 RNF-03 Implementado ✅
RF-04 (Docker Compose) US-03, US-04 RNF-01, RNF-02 Implementado ✅
RF-05 (Feedback Visual) US-01 RNF-03 Implementado ✅

Visão Geral da Arquitetura e Tecnologias

Nossa aplicação seguirá uma arquitetura em camadas, padrão em projetos Spring Boot, para garantir a separação de responsabilidades e a manutenibilidade do código:

2. Configurando o Ambiente de Desenvolvimento

Antes de começarmos a codificar, vamos preparar nosso ambiente:

  1. Java Development Kit (JDK) 21: Certifique-se de ter o JDK 21 instalado e configurado em sua máquina.
  2. Maven ou Gradle: Utilize sua ferramenta de build de preferência. Este guia utilizará a sintaxe do Maven.
  3. IDE (Ambiente de Desenvolvimento Integrado): Recomendamos o uso do IntelliJ IDEA, Eclipse ou Visual Studio Code com as extensões Java e Spring Boot.
  4. Spring Initializr: A maneira mais fácil de iniciar um projeto Spring Boot é através do Spring Initializr.

Configuração no Spring Initializr:

Após preencher as informações, clique em “Generate” para baixar o projeto base.


3. Estrutura do Projeto

Após importar o projeto para sua IDE, teremos a seguinte estrutura de pacotes:

src
└── main
    ├── java
    │   └── br
    │       └── com
    │           └── bibliotecajogos
    │               ├── BibliotecaJogosApplication.java
    │               ├── config    
    │               ├── controller
    │               ├── entity
    │               ├── repository
    │               └── service
    └── resources
        ├── static
        ├── templates
        └── application.properties
        └── application-dev.properties
        └── application-prod.properties                        

Abaixo está o modelo de banco de dados e o relacionamento entre as entidades Categoria e Jogo, representados em um Diagrama de Entidade-Relacionamento (ERD).

Este diagrama reflete exatamente a estrutura que definimos em nossas classes Entity com JPA.


Diagrama de Entidade-Relacionamento (ERD)

erDiagram
    CATEGORIA ||--o{ JOGO : "possui"
    CATEGORIA {
    BIGINT id PK "Chave Primária"
    VARCHAR nome "Nome da categoria (ex: RPG, Ação)"
    }
    JOGO {
    BIGINT id PK "Chave Primária"
    VARCHAR titulo "Título do jogo"
    VARCHAR autor "Desenvolvedora ou Autor"
    INT anoPublicacao "Ano de lançamento"
    VARCHAR genero "Gênero específico (ex: RPG de Ação)"
    BOOLEAN finalizado "Indica se o jogo foi concluído"
    BIGINT categoria_id FK "Chave Estrangeira para Categoria"
    }

Abaixo está um diagrama Mermaid que ilustra o fluxo de navegação e a relação entre as páginas HTML da aplicação “Biblioteca de Jogos”.

Diagrama de Fluxo das Páginas (Views)

Este diagrama mostra como o usuário navega entre a tela de listagem e a tela de formulário, e quais ações disparam cada transição.

flowchart TD
    A["Página: Lista de Jogos (jogos.html)"] -->|"Clica em 'Adicionar'"| B["Página: Formulário (formulario-jogo.html)"]
    A -->|Clica em 'Editar'| B
    B -->|Clica em 'Salvar'| A
    B -->|Clica em 'Cancelar'| A
    A -->|Clica em 'Excluir'| A
    A -->|Submete 'Pesquisa'| A

Explicação do Diagrama

  1. Template Base (layout.html): O diagrama mostra que tanto a “Lista de Jogos” (jogos.html) quanto o “Formulário de Jogo” (formulario-jogo.html) são envolvidos pelo layout.html, que fornece a estrutura visual comum (menu de navegação, rodapé, etc.).

  2. Página: Lista de Jogos (jogos.html):

    • É a tela principal e o ponto de partida (/jogos).
    • A partir dela, o usuário pode iniciar três fluxos principais:
      • Adicionar: Leva para a página de formulário.
      • Editar: Também leva para a mesma página de formulário, mas com os dados do jogo pré-carregados.
      • Excluir e Pesquisar: São ações que executam uma lógica no backend e, em seguida, recarregam a própria página de lista, seja para remover um item ou para exibir um resultado filtrado. Por isso, as setas apontam de volta para a própria página.
  3. Página: Formulário de Jogo (formulario-jogo.html):

    • É a tela usada tanto para criação quanto para edição.
    • Possui duas saídas principais:
      • Salvar: Envia os dados do formulário para o servidor (POST /jogos) e, após o processamento, redireciona o usuário de volta para a lista de jogos atualizada.
      • Cancelar: Simplesmente retorna o usuário para a lista de jogos, descartando quaisquer alterações.

Uma versão mais detalhada e didática do diagrama é uma ótima maneira de solidificar o conhecimento para estudantes.

A versão anterior era funcional, mas esta nova versão focará em explicar o “porquê” das coisas acontecerem, incluindo os métodos HTTP e o fluxo de requisição e resposta entre o navegador e o servidor.

Diagrama de Fluxo Acadêmico (Navegador ↔ Servidor)

Este diagrama separa visualmente o que acontece no navegador do usuário (Client-Side) e o que acontece no servidor (Server-Side), detalhando as requisições HTTP que conectam os dois.

flowchart TD
    subgraph "Lado do Cliente (Navegador)"
    A["Página: Lista de Jogos<br>(<b>GET</b> /jogos)"]
    B["Página: Formulário<br>(<b>GET</b> /jogos/novo ou /jogos/editar/{id})"]
    end
    subgraph "Lado do Servidor (Spring Boot)"
    C["Controller processa<br><b>POST</b> /jogos"]
    D["Controller processa<br><b>GET</b> /jogos/excluir/{id}"]
    E["Redirecionamento<br>(HTTP 302 Found)"]
    end
    %% Fluxos de Navegação
    A --"1. Clica em 'Adicionar' ou 'Editar'"--> B
    B --"2. Clica em 'Salvar'"--> C
    C --"3. Salva no Banco de Dados"--> E
    A --"4. Clica em 'Excluir'"--> D
    D --"5. Exclui do Banco de Dados"--> E
    E --"6. Navegador é redirecionado para a lista"--> A
    B --"7. Clica em 'Cancelar' (Link simples)"--> A
    %% Estilos para clareza
    style A fill:#cde4ff,stroke:#333
    style B fill:#e8dff5,stroke:#333
    style C fill:#fff5cc,stroke:#333
    style D fill:#fff5cc,stroke:#333
    style E fill:#d4edda,stroke:#333

Explicação Didática do Diagrama

Este modelo vai além de simplesmente mostrar a navegação, ele ensina conceitos fundamentais de desenvolvimento web:

  1. Separação Cliente-Servidor: O diagrama mostra claramente que existem dois “mundos”: o que o usuário vê e interage no Navegador e o que a aplicação Spring Boot processa no Servidor.

  2. Métodos HTTP e URLs:

    • Para visualizar páginas, o navegador sempre faz uma requisição GET para uma URL específica (ex: GET /jogos).
    • Para enviar dados (criar ou atualizar um jogo), o formulário faz uma requisição POST para /jogos. O método POST é usado para alterar dados no servidor.
  3. O Padrão Post-Redirect-Get (PRG): Este é um dos conceitos mais importantes ilustrados aqui.

    • Observe o fluxo ao “Salvar” (2 → 3 → 6):
      1. O navegador envia os dados (POST).
      2. O Servidor recebe, processa e salva no banco (Controller processa POST /jogos).
      3. Crucial: Em vez de devolver uma página HTML diretamente, o servidor responde com um Redirecionamento (HTTP 302).
      4. O Navegador recebe essa resposta e “obedece”, fazendo uma nova requisição GET para a página de listagem (/jogos), que agora mostrará os dados atualizados.
    • Por que isso é importante? Isso evita o reenvio acidental de formulários. Se o usuário atualizasse a página após salvar, ele estaria atualizando a página de listagem (GET), e não reenviando os dados do formulário (POST). O mesmo padrão se aplica à exclusão.
  4. Ações de Estado vs. Ações de Navegação:

    • “Salvar” e “Excluir” são ações que mudam o estado dos dados no servidor. Por isso, elas passam por um processamento no backend antes de redirecionar.
    • “Cancelar” é uma simples ação de navegação. É apenas um link (<a>) que leva o usuário de volta para a página de lista, sem a necessidade de processamento no servidor.

Este diagrama oferece uma visão muito mais completa e alinhada com os conceitos que um estudante de engenharia de software precisa entender sobre o funcionamento de uma aplicação web moderna.


Explicação do Modelo

1. Entidades (Tabelas)

2. Relacionamento

Este modelo simples, porém robusto, é a base da nossa aplicação, permitindo organizar os jogos de forma eficiente e realizar consultas e ordenações baseadas em suas categorias.


4. Modelando os Dados: Entidades

Vamos começar definindo as entidades que representarão nossos dados. Teremos duas entidades principais: Jogo e Categoria.

Categoria.java

package br.com.bibliotecajogos.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.List;

@Entity
public class Categoria {

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

    private String nome;

    @OneToMany(mappedBy = "categoria")
    private List<Jogo> jogos;

    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 List<Jogo> getJogos() {
        return jogos;
    }

    public void setJogos(List<Jogo> jogos) {
        this.jogos = jogos;
    }
}

Jogo.java

package br.com.bibliotecajogos.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;

@Entity
public class Jogo {

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

    private String titulo;
    private String autor;
    private Integer anoPublicacao;
    private String genero;
    private String urlCapa; // Novo campo

    private boolean finalizado = false;

    @ManyToOne
    @JoinColumn(name = "categoria_id")
    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 getUrlCapa() {
        return urlCapa;
    }

    public void setUrlCapa(String urlCapa) {
        this.urlCapa = urlCapa;
    }

    public boolean isFinalizado() {
        return finalizado;
    }

    public void setFinalizado(boolean finalizado) {
        this.finalizado = finalizado;
    }

    public Categoria getCategoria() {
        return categoria;
    }

    public void setCategoria(Categoria categoria) {
        this.categoria = categoria;
    }
}

Pontos importantes:

5. Camada de Acesso a Dados: Repositórios

Com as entidades definidas, vamos criar os repositórios para interagir com o banco de dados. O Spring Data JPA nos permite criar interfaces que, em tempo de execução, terão suas implementações geradas automaticamente.

CategoriaRepository.java

package br.com.bibliotecajogos.repository;

import br.com.bibliotecajogos.entity.Categoria;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoriaRepository extends JpaRepository<Categoria, Long> {
}

JogoRepository.java

package br.com.bibliotecajogos.repository;

import br.com.bibliotecajogos.entity.Jogo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface JogoRepository extends JpaRepository<Jogo, Long> {

    List<Jogo> findByTituloContainingIgnoreCase(String titulo);

    List<Jogo> findByAutorContainingIgnoreCase(String autor);

    List<Jogo> findByGeneroContainingIgnoreCase(String genero);
}

Entendendo o JogoRepository:

6. Lógica de Negócio: Camada de Serviço

A camada de serviço conterá a lógica de negócio da nossa aplicação.

JogoService.java

package br.com.bibliotecajogos.service;

import br.com.bibliotecajogos.entity.Jogo;
import br.com.bibliotecajogos.repository.JogoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class JogoService {

    @Autowired
    private JogoRepository jogoRepository;

    public List<Jogo> listarTodos(String sortBy) {
        return jogoRepository.findAll(Sort.by(sortBy));
    }

    public Jogo salvar(Jogo jogo) {
        return jogoRepository.save(jogo);
    }

    public Jogo buscarPorId(Long id) {
        return jogoRepository.findById(id).orElse(null);
    }

    public void excluir(Long id) {
        jogoRepository.deleteById(id);
    }

    public List<Jogo> pesquisar(String termo, String tipoPesquisa) {
        switch (tipoPesquisa) {
            case "titulo":
                return jogoRepository.findByTituloContainingIgnoreCase(termo);
            case "autor":
                return jogoRepository.findByAutorContainingIgnoreCase(termo);
            case "genero":
                return jogoRepository.findByGeneroContainingIgnoreCase(termo);
            default:
                return List.of();
        }
    }
}

Principais pontos:

7. Camada de Controle: Controllers

Os controllers são a porta de entrada da nossa aplicação. Eles recebem as requisições HTTP e as direcionam para a camada de serviço.

JogoController.java

package br.com.bibliotecajogos.controller;

import br.com.bibliotecajogos.entity.Jogo;
import br.com.bibliotecajogos.service.JogoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class JogoController {

    @Autowired
    private JogoService jogoService;

    @GetMapping("/jogos")
    public String listarJogos(Model model, @RequestParam(defaultValue = "titulo") String sortBy) {
        model.addAttribute("jogos", jogoService.listarTodos(sortBy));
        return "jogos";
    }

    @GetMapping("/jogos/novo")
    public String novoJogoForm(Model model) {
        model.addAttribute("jogo", new Jogo());
        return "formulario-jogo";
    }

    @PostMapping("/jogos")
    public String salvarJogo(@ModelAttribute("jogo") Jogo jogo) {
        jogoService.salvar(jogo);
        return "redirect:/jogos";
    }

    @GetMapping("/jogos/editar/{id}")
    public String editarJogoForm(@PathVariable Long id, Model model) {
        model.addAttribute("jogo", jogoService.buscarPorId(id));
        return "formulario-jogo";
    }

    @GetMapping("/jogos/excluir/{id}")
    public String excluirJogo(@PathVariable Long id) {
        jogoService.excluir(id);
        return "redirect:/jogos";
    }

    @GetMapping("/jogos/pesquisar")
    public String pesquisarJogos(@RequestParam String termo, @RequestParam String tipo, Model model) {
        model.addAttribute("jogos", jogoService.pesquisar(termo, tipo));
        return "jogos";
    }
}

Anotações importantes:


BibliotecajogosApplication.java

package br.com.bibliotecajogos;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan("br.com.bibliotecajogos.entity")
@EnableJpaRepositories("br.com.bibliotecajogos.repository")
public class BibliotecajogosApplication {

\public static void main(String[] args) {
	\SpringApplication.run(BibliotecajogosApplication.class, args);
	}

}


8. Camada de Apresentação: Views com Thymeleaf

Agora, vamos criar as páginas HTML que serão exibidas para o usuário.

src/main/resources/templates/jogos.html

📄 web-spring/src/main/resources/templates/jogos.html (arquivo real do projeto)

<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{layout :: layout (title=~{::title}, content=~{::section})}"
>
  <head>
    <title>Biblioteca de Jogos</title>
  </head>
  <body>
    <section>
      <div class="d-flex justify-content-between align-items-center mb-4">
        <h1 class="mb-0">Listagem de Jogos</h1>
        <a th:href="@{/jogos/novo}" class="btn btn-primary"
          >Adicionar Novo Jogo</a
        >
      </div>

      <form th:action="@{/jogos/pesquisar}" method="get" class="mb-4">
        <div class="input-group">
          <input
            type="text"
            name="termo"
            class="form-control"
            placeholder="Pesquisar..."
          />
          <select name="tipo" class="form-select">
            <option value="titulo">Título</option>
            <option value="autor">Autor</option>
            <option value="genero">Gênero</option>
          </select>
          <button type="submit" class="btn btn-outline-secondary">
            Pesquisar
          </button>
        </div>
      </form>

      <table class="table table-striped table-hover">
        <thead class="table-dark">
          <tr>
            <th>Capa</th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='titulo')}"
                >Título</a
              >
            </th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='autor')}"
                >Autor</a
              >
            </th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='anoPublicacao')}"
                >Ano</a
              >
            </th>
            <th>Gênero</th>
            <th>Finalizado</th>
            <th>Ações</th>
          </tr>
        </thead>
        <tbody>
          <tr th:each="jogo : ${jogos}">
            <td>
              <img
// … (+42 linhas: veja o arquivo completo no projeto)

src/main/resources/templates/formulario-jogo.html

📄 web-spring/src/main/resources/templates/formulario-jogo.html (arquivo real do projeto)

<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{layout :: layout (title=~{::title}, content=~{::section})}"
>
  <head>
    <title th:text="${jogo.id == null ? 'Novo Jogo' : 'Editar Jogo'}">
      Formulário de Jogo
    </title>
  </head>
  <body>
    <section>
      <h1 th:text="${jogo.id == null ? 'Adicionar Novo Jogo' : 'Editar Jogo'}">
        Formulário
      </h1>

      <div class="card">
        <div class="card-body">
          <form th:action="@{/jogos}" th:object="${jogo}" method="post">
            <input type="hidden" th:field="*{id}" />
            <div class="mb-3">
              <label for="titulo" class="form-label">Título:</label>
              <input
                type="text"
                id="titulo"
                class="form-control"
                th:field="*{titulo}"
              />
            </div>
            <div class="mb-3">
              <label for="autor" class="form-label">Autor:</label>
              <input
                type="text"
                id="autor"
                class="form-control"
                th:field="*{autor}"
              />
            </div>
            <div class="mb-3">
              <label for="anoPublicacao" class="form-label"
                >Ano de Publicação:</label
              >
              <input
                type="number"
                id="anoPublicacao"
                class="form-control"
                th:field="*{anoPublicacao}"
              />
            </div>
            <div class="mb-3">
              <label for="genero" class="form-label">Gênero:</label>
              <input
                type="text"
                id="genero"
                class="form-control"
                th:field="*{genero}"
              />
            </div>
            <div class="mb-3">
              <label for="urlCapa" class="form-label">URL da Capa:</label>
              <input
                type="text"
                id="urlCapa"
                class="form-control"
                th:field="*{urlCapa}"
              />
            </div>
            <div class="form-check mb-3">
              <input
                type="checkbox"
// … (+18 linhas: veja o arquivo completo no projeto)

Sintaxe do Thymeleaf:

9. Configuração do Banco de Dados

Agora, vamos configurar os bancos de dados para os ambientes de desenvolvimento e produção.

src/main/resources/application.properties

spring.application.name=bibliotecajogos
# --- H2 (Desenvolvimento) ---
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Forca a recriacao do banco de dados a cada inicializacao para garantir que os dados de teste sejam sempre atualizados
spring.jpa.hibernate.ddl-auto=create

# --- PostgreSQL (Producao) ---
#spring.datasource.url=jdbc:postgresql://localhost:5432/bibliotecajogos
#spring.datasource.username=seu_usuario
#spring.datasource.password=sua_senha
#spring.jpa.hibernate.ddl-auto=update
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# --- Configuracao Ativa (Desenvolvimento) ---
spring.profiles.active=dev

src/main/resources/application-dev.properties

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

src/main/resources/application-prod.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/bibliotecajogos
spring.datasource.username=seu_usuario
spring.datasource.password=sua_senha
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

Explicação:


pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
\xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>br.com.bibliotecajogos</groupId>
	<artifactId>bibliotecajogos</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>bibliotecajogos</name>
	<description>Demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>


Estrutura do Projeto (Atulizado)

Após importar o projeto para sua IDE, teremos a seguinte estrutura de pacotes:

src
└── main
    ├── java
    │   └── br
    │       └── com
    │           └── bibliotecajogos
    │               ├── BibliotecajogosApplication.java
    │               ├── config
    │               │   └── DataInitializer.java
    │               ├── controller
    │               │   └── JogoController.java
    │               ├── entity
    │               │   ├── Categoria.java
    │               │   └── Jogo.java
    │               ├── repository
    │               │   ├── CategoriaRepository.java
    │               │   └── JogoRepository.java
    │               └── service
    │                   └── JogoService.java
    │               ├── BibliotecaJogosApplication.java    
    └── resources
        ├── static/
        ├── templates
        │   ├── formulario-jogo.html
        │   ├── jogos.html
        │   └── layout.html
        ├── application.properties
        ├── application-dev.properties
        └── application-prod.properties

10. Executando e Testando a Aplicação

  1. Execute a aplicação: Rode a classe BibliotecaJogosApplication.java a partir da sua IDE.
  2. Acesse a aplicação: Abra seu navegador e acesse http://localhost:8080/jogos.
  3. Teste as funcionalidades:
    • Adicione novos jogos.
    • Edite jogos existentes.
    • Exclua jogos.
    • Teste a ordenação clicando nos cabeçalhos da tabela.
    • Utilize a barra de pesquisa para encontrar jogos por título, autor ou gênero.
    • Marque jogos como finalizados.

11. Próximos Passos e Melhorias

Com a base da aplicação construída, você pode explorar diversas melhorias:


Vamos adicionar as seções de “Melhorias na Interface do Usuário com Bootstrap” e “Containerização com Docker” ao guia, detalhando o passo a passo para cada uma.

12. Melhorias na Interface do Usuário com Bootstrap

Para deixar nossa aplicação com uma aparência mais profissional e responsiva sem muito esforço, vamos integrar o Bootstrap, um dos frameworks CSS mais populares do mundo.

Passo 1: Adicionar o Bootstrap ao Projeto

A forma mais simples de incluir o Bootstrap é utilizando um CDN (Content Delivery Network). Isso evita a necessidade de gerenciar os arquivos CSS e JavaScript localmente. Vamos criar um template base para centralizar a inclusão do Bootstrap e a estrutura principal da página.

Crie um novo arquivo: src/main/resources/templates/layout.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="pt-br" th:fragment="layout(title, content)">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title th:replace="${title}">Biblioteca de Jogos</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
      <div class="container-fluid">
        <a class="navbar-brand" th:href="@{/jogos}">Biblioteca de Jogos</a>
      </div>
    </nav>

    <main class="container">
      <div th:replace="${content}"></div>
    </main>

    <footer class="container text-center mt-5">
      <p>&copy;  Biblioteca de Jogos 2025</p>
    </footer>

    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

Passo 2: Atualizar as Views para Usar o Layout e as Classes do Bootstrap

Agora, vamos refatorar nossas páginas jogos.html e formulario-jogo.html para utilizar o novo layout e as classes de estilo do Bootstrap.

src/main/resources/templates/jogos.html (Atualizado)

<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{layout :: layout (title=~{::title}, content=~{::section})}"
>
  <head>
    <title>Biblioteca de Jogos</title>
  </head>
  <body>
    <section>
      <div class="d-flex justify-content-between align-items-center mb-4">
        <h1 class="mb-0">Listagem de Jogos</h1>
        <a th:href="@{/jogos/novo}" class="btn btn-primary"
          >Adicionar Novo Jogo</a
        >
      </div>

      <form th:action="@{/jogos/pesquisar}" method="get" class="mb-4">
        <div class="input-group">
          <input
            type="text"
            name="termo"
            class="form-control"
            placeholder="Pesquisar..."
          />
          <select name="tipo" class="form-select">
            <option value="titulo">Título</option>
            <option value="autor">Autor</option>
            <option value="genero">Gênero</option>
          </select>
          <button type="submit" class="btn btn-outline-secondary">
            Pesquisar
          </button>
        </div>
      </form>

      <table class="table table-striped table-hover">
        <thead class="table-dark">
          <tr>
            <th>Capa</th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='titulo')}"
                >Título</a
              >
            </th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='autor')}"
                >Autor</a
              >
            </th>
            <th>
              <a
                class="text-white text-decoration-none"
                th:href="@{/jogos(sortBy='anoPublicacao')}"
                >Ano</a
              >
            </th>
            <th>Gênero</th>
            <th>Finalizado</th>
            <th>Ações</th>
          </tr>
        </thead>
        <tbody>
          <tr th:each="jogo : ${jogos}"> <td> <img th:if="${jogo.urlCapa != null and !jogo.urlCapa.isEmpty()}"
                th:src="${jogo.urlCapa}" alt="Capa do Jogo" width="50" onerror="this.onerror=null;this.src='https://via.placeholder.com/50x75.png?text=Capa';" /> <span th:if="${jogo.urlCapa == null or jogo.urlCapa.isEmpty()}"
                >Sem capa</span
              >
            </td>
            <td th:text="${jogo.titulo}"></td> <td th:text="${jogo.autor}"></td>
            <td th:text="${jogo.anoPublicacao}"></td> <td th:text="${jogo.genero}"></td>
            <td>
              <span th:if="${jogo.finalizado}" class="badge bg-success" >Sim</span > <span th:unless="${jogo.finalizado}"
                class="badge bg-warning text-dark"
                >Não</span
              >
            </td>
            <td>
              <a
                th:href="@{/jogos/editar/{id}(id=${jogo.id})}" class="btn btn-sm btn-info" >Editar</a > <a th:href="@{/jogos/excluir/{id}(id=${jogo.id})}"
                class="btn btn-sm btn-danger"
                >Excluir</a
              >
            </td>
          </tr>
        </tbody>
      </table>
    </section>
  </body>
</html>

src/main/resources/templates/formulario-jogo.html (Atualizado)

<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{layout :: layout (title=~{::title}, content=~{::section})}"
>
  <head>
    <title th:text="${jogo.id == null ? 'Novo Jogo' : 'Editar Jogo'}"> Formulário de Jogo </title> </head> <body> <section> <h1 th:text="${jogo.id == null ? 'Adicionar Novo Jogo' : 'Editar Jogo'}">
        Formulário
      </h1>

      <div class="card">
        <div class="card-body">
          <form th:action="@{/jogos}" th:object="${jogo}" method="post">
            <input type="hidden" th:field="*{id}" />
            <div class="mb-3">
              <label for="titulo" class="form-label">Título:</label>
              <input
                type="text"
                id="titulo"
                class="form-control"
                th:field="*{titulo}"
              />
            </div>
            <div class="mb-3">
              <label for="autor" class="form-label">Autor:</label>
              <input
                type="text"
                id="autor"
                class="form-control"
                th:field="*{autor}"
              />
            </div>
            <div class="mb-3">
              <label for="anoPublicacao" class="form-label"
                >Ano de Publicação:</label
              >
              <input
                type="number"
                id="anoPublicacao"
                class="form-control"
                th:field="*{anoPublicacao}"
              />
            </div>
            <div class="mb-3">
              <label for="genero" class="form-label">Gênero:</label>
              <input
                type="text"
                id="genero"
                class="form-control"
                th:field="*{genero}"
              />
            </div>
            <div class="mb-3">
              <label for="urlCapa" class="form-label">URL da Capa:</label>
              <input
                type="text"
                id="urlCapa"
                class="form-control"
                th:field="*{urlCapa}"
              />
            </div>
            <div class="form-check mb-3">
              <input
                type="checkbox"
                id="finalizado"
                class="form-check-input"
                th:field="*{finalizado}"
              />
              <label for="finalizado" class="form-check-label"
                >Finalizado</label
              >
            </div>
            <div>
              <button type="submit" class="btn btn-success">Salvar</button>
              <a th:href="@{/jogos}" class="btn btn-secondary">Cancelar</a>
            </div>
          </form>
        </div>
      </div>
    </section>
  </body>
</html>

Após aplicar essas mudanças, reinicie a aplicação. Você verá uma interface muito mais moderna e agradável.


13. Containerização com Docker

Containerizar nossa aplicação com Docker nos permite empacotar o software com todas as suas dependências em uma unidade padronizada para desenvolvimento, envio e implantação. Isso garante que a aplicação funcione da mesma forma em qualquer ambiente.

Um diagrama é uma excelente forma de visualizar a arquitetura completa do projeto que construímos, incluindo todos os componentes e suas interações.

Aqui está um diagrama Mermaid que representa a estrutura geral do nosso sistema, desde a interação do usuário até a infraestrutura em contêineres com Docker.

Diagrama de Arquitetura do Projeto

flowchart TD
    subgraph "Ambiente Docker"
    direction LR
    subgraph "Container: App Spring Boot"
    direction TB
    A["Controller"] --> B["Service"]
    B --> C["Repository / JPA"]
    end
    subgraph "Container: Banco de Dados"
    direction TB
    DB[("PostgreSQL")]
    end
    subgraph "Container: Gerenciador de DB"
    direction TB
    PGADMIN["pgAdmin"]
    end
    C --> DB
    PGADMIN -.-> DB
    end
    USER["Usuário"] -->|Requisição HTTP| A
    A -->|HTML com Thymeleaf| USER
    style USER fill:#f9f,stroke:#333,stroke-width:2px
    style DB fill:#add,stroke:#333,stroke-width:2px
    style PGADMIN fill:#f8d5a2,stroke:#333,stroke-width:2px

Explicação do Diagrama

Este diagrama de fluxo ilustra os seguintes pontos-chave da nossa arquitetura:

  1. Usuário: Representa o cliente final interagindo com a aplicação através de um navegador web.
  2. Ambiente Docker: É o bloco maior, indicando que todos os nossos serviços (aplicação, banco de dados e pgAdmin) estão rodando como contêineres isolados, mas conectados, gerenciados pelo Docker Compose.
  3. Container: App Spring Boot:
    • Controller: A porta de entrada da aplicação. Recebe as requisições HTTP do usuário.
    • Service: Onde a lógica de negócio é processada.
    • Repository / JPA: A camada de acesso aos dados, responsável por se comunicar com o banco de dados.
    • O fluxo Controller -> Service -> Repository demonstra a arquitetura em camadas da nossa aplicação.
  4. Container: Banco de Dados:
    • PostgreSQL: Nosso banco de dados relacional que armazena todos os dados da aplicação (jogos, categorias, etc.). A aplicação se comunica com ele através da camada de repositório.
  5. Container: Gerenciador de DB:
    • pgAdmin: A ferramenta de administração gráfica. A linha pontilhada (-.->) indica que ele se conecta ao banco de dados para fins de gerenciamento e visualização, mas não faz parte do fluxo principal da aplicação.

O diagrama mostra claramente como uma requisição do usuário viaja através das camadas da aplicação Spring Boot para ler ou escrever no banco de dados PostgreSQL, e como o pgAdmin oferece uma interface de gerenciamento paralela, tudo orquestrado dentro do ambiente Docker.

Passo 1: Criar o Dockerfile

Na raiz do seu projeto, crie um arquivo chamado Dockerfile (sem extensão).

Dockerfile

# Estágio 1: Build da aplicação com Maven
# Usamos uma imagem que já contém o JDK e o Maven
FROM maven:3.9.6-eclipse-temurin-21 AS build

# Define o diretório de trabalho dentro do container
WORKDIR /app

# Copia os arquivos de configuração do Maven para aproveitar o cache de dependências
COPY pom.xml .
COPY .mvn .mvn

# Baixa as dependências do projeto
RUN mvn dependency:go-offline

# Copia o restante do código fonte
COPY src ./src

# Executa o build do projeto, gerando o arquivo .jar
# -DskipTests para pular a execução dos testes durante o build da imagem
RUN mvn package -DskipTests

# Estágio 2: Criação da imagem final
# Usamos uma imagem mais leve, contendo apenas o JRE, para rodar a aplicação
FROM eclipse-temurin:21-jre-jammy

# Define o diretório de trabalho
WORKDIR /app

# Copia o arquivo .jar gerado no estágio de build para a imagem final
COPY --from=build /app/target/*.jar app.jar

# Expõe a porta que a aplicação Spring Boot usa por padrão
EXPOSE 8080

# Comando para iniciar a aplicação quando o container for executado
ENTRYPOINT ["java", "-jar", "app.jar"]

Explicação do Dockerfile (Multi-stage build):

Passo 2: Criar o arquivo .dockerignore

Para evitar que arquivos desnecessários sejam copiados para o contexto de build do Docker, crie um arquivo .dockerignore na raiz do projeto.

.dockerignore

.git
.idea
target/

Passo 3: Construir e Rodar a Imagem Docker

Abra um terminal na raiz do projeto e execute os seguintes comandos:

  1. Construir a imagem Docker:

    docker build -t bibliotecajogos .
    
    • docker build: Comando para construir a imagem.
    • -t bibliotecajogos: Atribui um nome (tag) à imagem.
    • .: Indica que o contexto do build (onde o Dockerfile está) é o diretório atual.
  2. Rodar o container:

    docker run -p 8080:8080 bibliotecajogos
    
    • docker run: Comando para criar e iniciar um container a partir de uma imagem.
    • -p 8080:8080: Mapeia a porta 8080 do seu computador (host) para a porta 8080 do container.
    • ``: O nome da imagem que você quer rodar.

Agora, você pode acessar a aplicação em http://localhost:8080/jogos exatamente como antes, mas desta vez ela está rodando dentro de um container Docker!

Passo 4 (Avançado): Orquestrando com Docker Compose

Para um ambiente de produção, é comum rodar tanto a aplicação quanto o banco de dados em containers separados. O Docker Compose é a ferramenta ideal para definir e gerenciar aplicações Docker multi-container.

  1. Crie o arquivo docker-compose.yml na raiz do projeto:

    version: "3.8"
    
    services:
      # Serviço do banco de dados PostgreSQL
      db:
        image: postgres:17
        container_name: postgres-db
        environment:
          POSTGRES_USER: seu_usuario # Mesmo usuário do application-prod.properties
          POSTGRES_PASSWORD: sua_senha # Mesma senha do application-prod.properties
          POSTGRES_DB: bibliotecajogos # Mesmo nome do banco do application-prod.properties
        ports:
          - "5432:5432"
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
      # Serviço da nossa aplicação
      app:
        build: . # Indica para construir a imagem a partir do Dockerfile no diretório atual
        container_name: bibliotecajogos-app
        depends_on:
          - db # Garante que o serviço 'db' inicie antes do serviço 'app'
        ports:
          - "8080:8080"
        environment:
          # Sobrescreve as configurações para apontar para o container do banco de dados
          - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/bibliotecajogos
          - SPRING_DATASOURCE_USERNAME=seu_usuario
          - SPRING_DATASOURCE_PASSWORD=sua_senha
          - SPRING_PROFILES_ACTIVE=prod # Ativa o perfil de produção
    
    volumes:
      postgres_data:
    
  2. Atualize o application-prod.properties:

    A única mudança necessária é no host do datasource URL, que agora será o nome do serviço do Docker (db). No entanto, passamos essa configuração via variáveis de ambiente no docker-compose.yml, o que é uma prática melhor.

  3. Rode com Docker Compose:

    No terminal, na raiz do projeto, execute:

    docker-compose up --build
    
    • docker-compose up: Inicia todos os serviços definidos no arquivo docker-compose.yml.
    • --build: Força a reconstrução da imagem da sua aplicação caso haja alguma alteração no código ou no Dockerfile.

Agora você tem sua aplicação e um banco de dados PostgreSQL rodando em containers isolados, mas conectados em rede, simulando um ambiente de produção de forma simples e replicável.


14. Populando o Banco de Dados com Dados Iniciais (Database Seeding)

Para facilitar os testes, vamos criar uma classe que insere alguns jogos e categorias no banco de dados assim que a aplicação é iniciada. Faremos isso de uma forma que só aconteça no ambiente de desenvolvimento (dev), evitando que esses dados sejam inseridos em produção.

A abordagem que usaremos é a implementação da interface CommandLineRunner. O Spring Boot executa automaticamente todos os beans do tipo CommandLineRunner uma vez, após o contexto da aplicação ter sido completamente inicializado.

Passo 1: Criar a Classe de Seed

Vamos criar uma nova classe para conter nossa lógica de seeding. É uma boa prática colocá-la em um pacote de configuração.

Crie o arquivo: src/main/java/br/com/bibliotecajogos/config/DataInitializer.java

package br.com.bibliotecajogos.config;

import br.com.bibliotecajogos.entity.Categoria;
import br.com.bibliotecajogos.entity.Jogo;
import br.com.bibliotecajogos.repository.CategoriaRepository;
import br.com.bibliotecajogos.repository.JogoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Profile("dev") // A anotação @Profile("dev") garante que este bean só será ativado quando o perfil 'dev' estiver ativo
public class DataInitializer implements CommandLineRunner {

    @Autowired
    private JogoRepository jogoRepository;

    @Autowired
    private CategoriaRepository categoriaRepository;

    @Override
    public void run(String... args) throws Exception {
        if (categoriaRepository.count() == 0) {
            System.out.println(">>> Populando banco de dados de desenvolvimento...");

            // Criar Categorias
            Categoria rpg = new Categoria();
            rpg.setNome("RPG");

            Categoria acao = new Categoria();
            acao.setNome("Ação");

            Categoria estrategia = new Categoria();
            estrategia.setNome("Estratégia");

            Categoria aventura = new Categoria();
            aventura.setNome("Aventura");

            categoriaRepository.saveAll(Arrays.asList(rpg, acao, estrategia, aventura));

            // Criar Jogos
            Jogo jogo1 = new Jogo();
            jogo1.setTitulo("The Witcher 3: Wild Hunt");
            jogo1.setAutor("CD Projekt Red");
            jogo1.setAnoPublicacao(2015);
            jogo1.setGenero("RPG de Ação");
            jogo1.setCategoria(rpg);
            jogo1.setFinalizado(true);
            jogo1.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/thumb/0/06/TW3_Wild_Hunt.png/270px-TW3_Wild_Hunt.png");

            Jogo jogo2 = new Jogo();
            jogo2.setTitulo("Red Dead Redemption 2");
            jogo2.setAutor("Rockstar Games");
            jogo2.setAnoPublicacao(2018);
            jogo2.setGenero("Ação-Aventura");
            jogo2.setCategoria(acao);
            jogo2.setFinalizado(true);
            jogo2.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/e/e7/Red_Dead_Redemption_2.png");

            Jogo jogo3 = new Jogo();
            jogo3.setTitulo("Age of Empires IV");
            jogo3.setAutor("Relic Entertainment");
            jogo3.setAnoPublicacao(2021);
            jogo3.setGenero("Estratégia em Tempo Real");
            jogo3.setCategoria(estrategia);
            jogo3.setFinalizado(false);
            jogo3.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/4/43/Capa_do_jogo_Age_of_Empires_IV.jpg");

            Jogo jogo4 = new Jogo();
            jogo4.setTitulo("Baldur's Gate 3");
            jogo4.setAutor("Larian Studios");
            jogo4.setAnoPublicacao(2023);
            jogo4.setGenero("RPG");
            jogo4.setCategoria(rpg);
            jogo4.setFinalizado(false);
            jogo4.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/1/18/Baldur%27s_Gate_III_Larian_Studios_key_art.png");

            Jogo jogo5 = new Jogo();
            jogo5.setTitulo("God of War Ragnarök");
            jogo5.setAutor("Santa Monica Studio");
            jogo5.setAnoPublicacao(2022);
            jogo5.setGenero("Ação-Aventura");
            jogo5.setCategoria(aventura);
            jogo5.setFinalizado(true);
            jogo5.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/a/a5/God_of_War_Ragnar%C3%B6k_capa.jpg");

            Jogo jogo6 = new Jogo();
            jogo6.setTitulo("Elden Ring");
            jogo6.setAutor("FromSoftware");
            jogo6.setAnoPublicacao(2022);
            jogo6.setGenero("RPG de Ação");
            jogo6.setCategoria(rpg);
            jogo6.setFinalizado(true);
            jogo6.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/0/0d/Elden_Ring_capa.jpg");

            Jogo jogo7 = new Jogo();
            jogo7.setTitulo("Cyberpunk 2077");
            jogo7.setAutor("CD Projekt Red");
            jogo7.setAnoPublicacao(2020);
            jogo7.setGenero("RPG de Ação");
            jogo7.setCategoria(rpg);
            jogo7.setFinalizado(true);
            jogo7.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/thumb/f/f7/Cyberpunk_2077_capa.png/270px-Cyberpunk_2077_capa.png");

            Jogo jogo8 = new Jogo();
            jogo8.setTitulo("StarCraft II: Wings of Liberty");
            jogo8.setAutor("Blizzard Entertainment");
            jogo8.setAnoPublicacao(2010);
            jogo8.setGenero("Estratégia em Tempo Real");
            jogo8.setCategoria(estrategia);
            jogo8.setFinalizado(true);
            jogo8.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/4/40/StarCraft_II_capa.jpg");

            Jogo jogo9 = new Jogo();
            jogo9.setTitulo("The Legend of Zelda: Tears of the Kingdom");
            jogo9.setAutor("Nintendo");
            jogo9.setAnoPublicacao(2023);
            jogo9.setGenero("Ação-Aventura");
            jogo9.setCategoria(aventura);
            jogo9.setFinalizado(false);
            jogo9.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/2/25/Zelda_TotK_capa.jpg");

            Jogo jogo10 = new Jogo();
            jogo10.setTitulo("Grand Theft Auto V");
            jogo10.setAutor("Rockstar Games");
            jogo10.setAnoPublicacao(2013);
            jogo10.setGenero("Ação-Aventura");
            jogo10.setCategoria(acao);
            jogo10.setFinalizado(true);
            jogo10.setUrlCapa("https://upload.wikimedia.org/wikipedia/pt/8/80/Grand_Theft_Auto_V_capa.png");

            jogoRepository.saveAll(Arrays.asList(jogo1, jogo2, jogo3, jogo4, jogo5, jogo6, jogo7, jogo8, jogo9, jogo10));

            System.out.println(">>> Banco de dados populado com sucesso!");
        } else {
            System.out.println(">>> O banco de dados já está populado. Nenhuma ação foi tomada.");
        }
    }
}

Explicação do Código

  1. @Component: Esta anotação transforma a classe em um bean gerenciado pelo Spring, permitindo que o framework a encontre e execute.
  2. @Profile("dev"): Esta é a parte mais importante. Ela instrui o Spring a criar e executar este bean apenas quando o perfil de execução ativo for dev. Como configuramos spring.profiles.active=dev no application.properties, este código rodará ao iniciar a aplicação no ambiente de desenvolvimento. Ao mudar o perfil para prod, este bean será ignorado.
  3. implements CommandLineRunner: Ao implementar esta interface, somos obrigados a sobrescrever o método run. Este método é o ponto de entrada da nossa lógica de seeding.
  4. @Autowired: Injetamos nossos repositórios (JogoRepository e CategoriaRepository) para termos acesso aos métodos de persistência no banco de dados.
  5. if (categoriaRepository.count() == 0): Adicionamos uma verificação simples para garantir que a lógica de seeding só execute uma vez. Como estamos usando o H2 em memória, a cada reinicialização os dados são perdidos, então a condição será sempre verdadeira. No entanto, se você estivesse usando um banco de dados persistente em desenvolvimento, essa verificação impediria a inserção de dados duplicados a cada reinicialização.
  6. Criação e Persistência: Criamos objetos Categoria e Jogo em Java e usamos o método saveAll() dos repositórios para salvá-los no banco de dados de forma eficiente.

Passo 2: Testar o Seeding

Agora, basta rodar sua aplicação novamente com o perfil dev ativo. Observe o console de logs da sua IDE ou terminal. Você deverá ver as mensagens que adicionamos:

>>> Populando banco de dados de desenvolvimento...
>>> Banco de dados populado com sucesso!

Ao acessar http://localhost:8080/jogos no seu navegador, a lista de jogos já estará preenchida com os dados que cadastramos no DataInitializer.


Adicionar o pgAdmin ao nosso ambiente Docker é uma maneira excelente de ter uma interface gráfica para visualizar e gerenciar nosso banco de dados PostgreSQL.

Vamos integrar o pgAdmin ao nosso docker-compose.yml e detalhar o passo a passo para acessá-lo e conectar ao banco da nossa aplicação.

1. Modificando o arquivo docker-compose.yml

Vamos adicionar um novo service para o pgAdmin no mesmo arquivo docker-compose.yml que criamos anteriormente.

O serviço do pgAdmin precisará de:

Abra o arquivo docker-compose.yml e adicione o serviço pgadmin da seguinte forma:

services:
  # Serviço do banco de dados PostgreSQL (já existente)
  db:
    image: postgres:17
    container_name: postgres-db
    environment:
      POSTGRES_USER: seu_usuario
      POSTGRES_PASSWORD: sua_senha
      POSTGRES_DB: bibliotecajogos
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  # Serviço da nossa aplicação (já existente)
  app:
    build: .
    container_name: biblioteca-jogos-app
    depends_on:
      - db
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/bibliotecajogos
      - SPRING_DATASOURCE_USERNAME=seu_usuario
      - SPRING_DATASOURCE_PASSWORD=sua_senha
      - SPRING_PROFILES_ACTIVE=prod

  # NOVO SERVIÇO: pgAdmin
  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin4-web
    environment:
      PGADMIN_DEFAULT_EMAIL: "admin@admin.com"  # Email para login no pgAdmin
      PGADMIN_DEFAULT_PASSWORD: "admin"         # Senha para login no pgAdmin
    ports:
      - "5050:80"  # Mapeia a porta 5050 do seu PC para a 80 do container
    depends_on:
      - db
    volumes:
      - pgadmin_data:/var/lib/pgadmin

volumes:
  postgres_data:
  pgadmin_data:  # Adiciona o volume para o pgAdmin

O que fizemos de novo?

  1. services.pgadmin: Adicionamos a definição de um novo contêiner chamado pgadmin.
  2. environment: Definimos um e-mail (PGADMIN_DEFAULT_EMAIL) e uma senha (PGADMIN_DEFAULT_PASSWORD) que serão usados como suas credenciais para acessar a interface web do pgAdmin.
  3. ports: - "5050:80": Mapeamos a porta 5050 do nosso computador (host) para a porta 80 dentro do contêiner do pgAdmin. Escolhemos 5050 para não conflitar com a nossa aplicação (8080) ou com o Postgres (5432).
  4. volumes: - pgadmin_data:/var/lib/pgadmin: Criamos um volume chamado pgadmin_data. Isso é muito importante para que suas configurações de servidor e outros dados do pgAdmin não sejam perdidos se o contêiner for recriado.
  5. volumes: pgadmin_data:: Declaramos o volume na seção principal de volumes no final do arquivo.

2. Passo a Passo para Acessar e Configurar o pgAdmin

Com o arquivo docker-compose.yml atualizado, siga os passos abaixo:

Passo 1: Iniciar os Serviços

No terminal, na raiz do projeto, pare qualquer execução anterior (Ctrl + C) e suba os serviços novamente. O Docker Compose irá detectar o novo serviço pgadmin e o criará.

docker-compose up --build

Aguarde até que os logs indiquem que os três contêineres (db, app e pgadmin) estão rodando.

Passo 2: Acessar a Interface Web do pgAdmin

Abra seu navegador e acesse a URL:

http://localhost:5050

Você verá a tela de login do pgAdmin.

Passo 3: Fazer o Login

Use as credenciais que você definiu no arquivo docker-compose.yml:

Passo 4: Conectar ao Banco de Dados PostgreSQL

Após o login, você verá a tela inicial. Agora, vamos registrar o nosso servidor de banco de dados (db).

  1. Clique em “Add New Server”.

  2. Na aba “General”, dê um nome para a sua conexão. Pode ser qualquer nome, por exemplo: Biblioteca Jogos Local.

  3. Vá para a aba “Connection” e preencha os dados do seu contêiner do PostgreSQL. Esta é a parte mais importante!

    • Host name/address: db
      • Por que db? Porque é o nome do serviço do PostgreSQL definido no nosso docker-compose.yml. Dentro da rede do Docker Compose, os contêineres podem se comunicar usando os nomes dos serviços como se fossem nomes de host (DNS). Não use localhost aqui!
    • Port: 5432 (a porta padrão do Postgres).
    • Maintenance database: bibliotecajogos (o nome do banco que definimos).
    • Username: seu_usuario (o usuário que definimos).
    • Password: sua_senha (a senha que definimos).
    • Marque a opção “Save password?”.

    A sua tela deve ficar assim:

  4. Clique em “Save”.

Pronto!

Se tudo deu certo, o servidor aparecerá na barra lateral esquerda. Agora você pode navegar pelas Databases, Schemas e Tabelas para visualizar os dados que a sua aplicação está inserindo, executar queries SQL diretamente e muito mais.



🚀 Roteiro de Execução

1. Iniciar o Ambiente Docker (PostgreSQL & pgAdmin)

cd catalogo_projetos/javaweb_jogos_guia_crud/web-spring
docker-compose up -d

2. Iniciar a Aplicação Spring Boot

./mvnw spring-boot:run

No Windows (PowerShell):

cd catalogo_projetos\javaweb_jogos_guia_crud\web-spring
.\mvnw.cmd spring-boot:run

3. Acessar os Painéis


🎯 Matriz de Checkpoints de Entrega

Checkpoint Critério de Aceite Validação Status
CP-01 Subida do ambiente de banco de dados via Docker Compose Contêineres db e pgadmin em execução saudável PASS ✅
CP-02 Inicialização da aplicação Spring Boot 3 com migração de schema Tabelas categoria e jogo criadas no PostgreSQL PASS ✅
CP-03 Execução do DataInitializer populando base de demonstração Catálogo inicial de jogos visível na rota / PASS ✅
CP-04 Operações de CRUD validadas com persistência transacional Inserções, edições e remoções refletidas no banco PASS ✅
CP-05 Conexão e inspeção de dados através do console pgAdmin Acesso ao schema public via navegador web PASS ✅