PROGRAMA DE CURSO APOSTILA · API REST com Spring Boot
← Início

API REST — do Zero ao CRUD Completo

Apostila completa para construir uma API RESTful com Spring Boot usando o projeto SistemaVollMed como base real de aprendizado. Médicos, pacientes, consultas, relacionamentos e paginação — passo a passo.

Spring Boot 4.0.5 Java 17 Spring Data JPA H2 Database Lombok Bean Validation Records Stream API
1

O que é o projeto SistemaVollMed?

O SistemaVollMed é uma API REST para gerenciamento de uma clínica médica. Ele permite cadastrar médicos, pacientes e agendar consultas entre eles, tudo através de requisições HTTP usando ferramentas como o Insomnia ou Postman.

O que é uma API REST?

Uma API (Application Programming Interface) é um conjunto de "portas de entrada" que outros sistemas podem usar para conversar com a sua aplicação. O estilo REST (Representational State Transfer) define regras de como essas portas devem funcionar: usando os verbos do protocolo HTTP (GET, POST, PUT, DELETE) e trocando dados no formato JSON.

Analogia do Garçom Pense na API como um garçom de restaurante: você (o cliente / frontend) faz um pedido ao garçom, ele leva para a cozinha (o banco de dados), e traz o resultado de volta. Você nunca entra na cozinha diretamente. A API define o cardápio (endpoints) e as regras do pedido (formato JSON, verbos HTTP).

O que é CRUD?

CRUD é o acrônimo das 4 operações fundamentais de qualquer sistema que persiste dados:

C — Create

Criar

Inserir um novo registro no banco. Verbo HTTP: POST. Ex: cadastrar um médico.

R — Read

Ler

Consultar registros existentes. Verbo HTTP: GET. Ex: listar todos os médicos.

U — Update

Atualizar

Modificar um registro existente. Verbo HTTP: PUT. Ex: alterar o telefone de um médico.

D — Delete

Excluir

Remover um registro. Verbo HTTP: DELETE. Ex: excluir um paciente cadastrado.

O que o SistemaVollMed faz?

Recurso 1

Medicos

Cadastrar, listar (com paginação), atualizar e excluir médicos com especialidade, CRM e endereço embutido.

Recurso 2

Pacientes

Cadastrar, listar, atualizar e excluir pacientes com CPF e endereço. Segue o mesmo padrão do Médico.

Recurso 3

Consultas

Agendar consultas vinculando um médico a um paciente com data, status e observação.

Recurso 4

Paginacao

Listagem paginada para não retornar milhares de registros de uma vez — parâmetros page, size, sort.

Recurso 5

Validacao

Bean Validation com @NotBlank, @Email, @Valid para garantir integridade dos dados de entrada.

Recurso 6

Exclusao Logica

Alternar o campo "ativo" em vez de deletar definitivamente — preserva histórico de consultas.

Estrutura de pacotes do projeto

src/main/java/com/github/app/ ├── controller/ ← classes que recebem requisições HTTP │ ├── OlaController.java ← primeiro endpoint hello world (GET /ola) │ ├── MedicoController.java ← CRUD completo de médicos │ ├── PacienteController.java ← CRUD completo de pacientes │ └── ConsultaController.java ← agendamento de consultas │ ├── model/ │ ├── medico/ ← tudo relacionado ao médico │ │ ├── Medico.java ← @Entity: representa a tabela "medicos" │ │ ├── MedicoRepository.java ← acesso ao banco (extends JpaRepository) │ │ ├── Especialidade.java ← enum: ORTOPEDIA, CARDIOLOGIA etc. │ │ ├── DadosCadastroMedico.java ← DTO de entrada do POST │ │ ├── DadosListagemMedico.java ← DTO de saída do GET │ │ └── DadosAtualizacaoMedico.java ← DTO de entrada do PUT │ │ │ ├── paciente/ ← mesma estrutura do médico │ │ ├── Paciente.java │ │ ├── PacienteRepository.java │ │ ├── DadosCadastroPaciente.java │ │ ├── DadosListagemPaciente.java │ │ └── DadosAtualizacaoPaciente.java │ │ │ ├── consulta/ ← entidade com relacionamentos @ManyToOne │ │ ├── Consulta.java ← tem FK para medico e paciente │ │ ├── ConsultaRepository.java │ │ ├── Status.java ← enum: AGENDADA, CONFIRMADA etc. │ │ └── DadosAgendamentoConsulta.java ← DTO com medicoId e pacienteId │ │ │ └── endereco/ ← classe @Embeddable reutilizada em Medico e Paciente │ ├── Endereco.java ← campos embutidos na tabela pai (não é tabela própria) │ └── DadosCadastroEndereco.java ← DTO de endereço com validações │ └── AppApplication.java ← ponto de entrada: main() + @SpringBootApplication src/main/resources/ ├── application.properties ← define qual perfil está ativo └── application-test.properties ← configuração H2 para ambiente de teste

Como as camadas se comunicam

1

Cliente (Insomnia / Postman / Frontend)

Envia uma requisição HTTP. Ex: POST /medicos com JSON no corpo. O cliente não sabe nada sobre banco de dados.

2

Controller (@RestController)

Recebe a requisição. Valida o DTO recebido (se houver @Valid). Chama o Repository para persistir ou consultar dados. Retorna a resposta HTTP.

3

Repository (JpaRepository)

Interface que o Spring Data JPA implementa automaticamente. Traduz chamadas Java (save, findAll) em SQL que vai para o banco.

4

Banco de Dados (H2 em memória)

Executa o SQL gerado. Retorna os dados. No perfil de teste usamos H2 — em produção seria MySQL ou PostgreSQL.

Resumo do fluxo Cliente → (JSON) → Controller → (Entidade/DTO) → Repository → (SQL) → Banco de Dados → (resultado) → Repository → (Entidade) → Controller → (JSON) → Cliente
2

Configurando o pom.xml

O pom.xml (Project Object Model) é o arquivo de configuração do Maven — a ferramenta de gerenciamento de dependências do projeto. Ele declara quais bibliotecas externas o projeto precisa, e o Maven as baixa automaticamente do repositório central (Maven Central).

O Spring Boot usa um conceito de parent: ao declarar spring-boot-starter-parent como pai, o projeto herda o gerenciamento de versões de centenas de dependências — você nunca precisa especificar versão manualmente para bibliotecas do ecossistema Spring.

<!-- ════ PARENT ════════════════════════════════════════════════════ -->
<!-- Define o Spring Boot como "pai" — ele gerencia todas as versões
     das dependências filhas e configura o plugin de build -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.0.5</version>   <!-- versão do Spring Boot -->
  <relativePath/>              <!-- busca no repositório Maven Central -->
</parent>

<!-- ════ IDENTIFICAÇÃO DO PROJETO ══════════════════════════════════ -->
<groupId>com.github</groupId>        <!-- pacote base da organização/desenvolvedor -->
<artifactId>app</artifactId>           <!-- nome do projeto (gera app.jar) -->
<version>0.0.1-SNAPSHOT</version>    <!-- versão em desenvolvimento -->

<!-- ════ PROPRIEDADES ══════════════════════════════════════════════ -->
<properties>
  <java.version>17</java.version>  <!-- versão do Java usada na compilação -->
</properties>

<!-- ════ DEPENDÊNCIAS ══════════════════════════════════════════════ -->
<dependencies>

  <!-- WEB MVC ──────────────────────────────────────────────────────
       Inclui: Tomcat embutido, Spring MVC, Jackson (JSON).
       Sem isso: não há servidor HTTP, @RestController não funciona -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
  </dependency>

  <!-- DATA JPA ─────────────────────────────────────────────────────
       Inclui: Hibernate, Spring Data JPA, JDBC.
       Sem isso: @Entity, @Repository, JpaRepository não funcionam -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>

  <!-- H2 DATABASE ──────────────────────────────────────────────────
       Banco de dados em memória. Perfeito para testes: não precisa
       instalar nada. scope=runtime: só existe ao executar, não compila.
       Sem isso: não há driver de banco → aplicação não inicia -->
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>  <!-- disponível somente em tempo de execução -->
  </dependency>

  <!-- LOMBOK ───────────────────────────────────────────────────────
       Gera em tempo de compilação: getters, setters, construtores,
       equals/hashCode, toString. Elimina ~70% do código boilerplate.
       optional=true: não é transitivo (outros projetos não herdam).
       Sem isso: precisa escrever todos os métodos manualmente -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>

  <!-- VALIDATION ───────────────────────────────────────────────────
       Habilita Bean Validation: @NotBlank, @Email, @NotNull, @Valid.
       Sem isso: as anotações de validação são ignoradas silenciosamente
       e dados inválidos chegam até o banco -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

  <!-- ACTUATOR ─────────────────────────────────────────────────────
       Expõe endpoints de monitoramento da aplicação:
       /actuator/health → status da app
       /actuator/info   → informações do projeto
       Útil para produção e integração com ferramentas de monitoramento -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>

  <!-- DEVTOOLS ─────────────────────────────────────────────────────
       Hot reload: detecta mudanças no código e reinicia a aplicação
       automaticamente. runtime+optional: não vai para produção.
       Sem isso: precisa parar e reiniciar manualmente a cada mudança -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
  </dependency>

  <!-- TEST ─────────────────────────────────────────────────────────
       Inclui JUnit 5, Mockito, Spring Test. scope=test: só em testes.
       Não vai para o jar de produção -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>

</dependencies>

<!-- ════ BUILD ══════════════════════════════════════════════════════ -->
<build>
  <plugins>
    <!-- Plugin que empacota a aplicação como JAR executável (fat jar) -->
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

Tabela de dependências: o que cada uma faz e o que quebra sem ela

DependênciaO que incluiO que quebra sem ela
starter-webmvcTomcat, Spring MVC, Jackson (JSON)Sem servidor HTTP. @RestController não funciona. Nenhum endpoint responde.
starter-data-jpaHibernate, Spring Data, JDBC@Entity, @Repository, JpaRepository não compilam. Sem acesso ao banco.
h2Driver H2 em memóriaSem driver de banco. A aplicação não sobe. Erro de DataSource.
lombokGeração de código em tempo de compilação@Getter, @Setter, @AllArgsConstructor não existem. Código não compila.
starter-validationHibernate Validator, Jakarta Validation@NotBlank, @Email, @Valid são ignorados. Dados inválidos entram no banco.
starter-actuatorEndpoints /actuator/*Sem monitoramento. /actuator/health retorna 404.
devtoolsHot reload, LiveReloadPrecisa reiniciar manualmente após cada mudança. Só afeta desenvolvimento.
starter-testJUnit 5, Mockito, Spring TestTestes automatizados não funcionam. (Não afeta runtime.)
O que é um "starter"? Um starter do Spring Boot é uma dependência que já reúne várias bibliotecas menores pré-configuradas. Em vez de adicionar Tomcat + Spring MVC + Jackson separadamente, você adiciona só spring-boot-starter-webmvc e o Spring Boot configura tudo automaticamente. Isso é o princípio convention over configuration.
3

Classe Principal — @SpringBootApplication

Todo projeto Spring Boot tem uma única classe principal que serve como ponto de entrada da aplicação. É aqui que o programa começa a executar. Ela fica na raiz do pacote principal para garantir que o @ComponentScan varra todos os subpacotes.

package com.github.app;
// O pacote raiz deve ser o mais alto da hierarquia — todos os
// subpacotes (controller, model...) serão varridos automaticamente

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// Dois imports: a classe que faz o boot e a anotação mágica

@SpringBootApplication
// A anotação mais importante: combina @Configuration + @EnableAutoConfiguration
// + @ComponentScan. Detalhe abaixo.
public class AppApplication {

    public static void main(String[] args) {
        // Ponto de entrada do programa Java (método main padrão)
        // O JVM chama este método ao executar o .jar

        SpringApplication.run(AppApplication.class, args);
        // SpringApplication.run() é quem dispara tudo:
        // 1. Cria o ApplicationContext (container de IoC)
        // 2. Inicia o servidor Tomcat embutido na porta 8080
        // 3. Registra todos os Controllers, Repositories e Beans
        // 4. Executa os CommandLineRunners (se houver)
        // AppApplication.class → diz ao Spring onde começar o scan
        // args → passa argumentos da linha de comando (ex: --server.port=9090)
    }
}

O que @SpringBootApplication combina?

Anotacao 1

@Configuration

Indica que esta classe pode declarar beans (objetos gerenciados pelo Spring). Equivalente a um arquivo XML de configuração do Spring antigo, mas em Java puro.

Anotacao 2

@EnableAutoConfiguration

O Spring inspeciona as dependências do pom.xml e configura automaticamente tudo que encontrar. Se vê o H2 no classpath, configura o DataSource. Se vê o WebMVC, configura o Tomcat.

Anotacao 3

@ComponentScan

Varre todos os pacotes abaixo do pacote da classe anotada. Encontra @RestController, @Repository, @Service, @Component e os registra como beans no container do Spring.

O que acontece quando você clica em "Run"?

1

JVM inicia e chama main()

O Java Virtual Machine localiza o método main e começa a execução.

2

SpringApplication cria o ApplicationContext

O container IoC (Inversion of Control) é criado. Ele será responsável por criar e gerenciar todos os objetos (beans) da aplicação.

3

@ComponentScan varre os pacotes

O Spring encontra todas as classes anotadas com @RestController, @Repository etc. e as registra no container como beans.

4

Auto Configuration executa

Com base nas dependências do classpath, o Spring configura: DataSource (H2), JPA/Hibernate, Tomcat, Jackson, Validation etc.

5

Hibernate cria as tabelas

O Hibernate lê as entidades @Entity e cria as tabelas no banco H2 em memória (ddl-auto=create ou update).

6

Tomcat sobe na porta 8080

O servidor HTTP embutido começa a escutar requisições em http://localhost:8080. Você vê "Started AppApplication" no console.

Por que a classe fica no pacote raiz? O @ComponentScan por padrão varre o pacote da classe anotada e todos os subpacotes. Se a AppApplication estiver em com.github.app, ela varre com.github.app.controller, com.github.app.model etc. Se estiver em um subpacote, os outros subpacotes no mesmo nível não seriam encontrados.
4

Primeiro Controller — OlaController

Um Controller é a classe Java que recebe as requisições HTTP vindas do cliente (Insomnia, Postman, browser) e retorna uma resposta. É a camada mais externa da aplicação — a "porta de entrada". Vamos começar com o mais simples possível.

package com.github.app.controller;
// Fica no subpacote "controller" — o @ComponentScan vai encontrá-lo

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// Três anotações do Spring MVC: definem o comportamento HTTP da classe

@RestController
// Combina @Controller + @ResponseBody.
// @Controller: registra a classe como um bean que trata requisições HTTP.
// @ResponseBody: serializa automaticamente o retorno do método para JSON/texto
// no corpo da resposta HTTP (sem isso, o Spring tentaria encontrar uma view HTML)

@RequestMapping("ola")
// Define o caminho base para TODOS os métodos desta classe.
// A URL completa será: http://localhost:8080/ola
// Pode conter múltiplos níveis: "api/v1/medicos"

public class OlaController {

    @GetMapping
    // Mapeia este método para requisições HTTP GET no caminho base (/ola).
    // Se fosse @GetMapping("teste") seria GET /ola/teste

    public String olaMundo() {
        // O método retorna uma String. Com @RestController, ela é
        // enviada diretamente no corpo da resposta HTTP como texto puro.
        // Se retornasse um objeto Java, o Jackson o converteria para JSON.

        return "Olá Mundo!";
        // Resposta: HTTP 200 OK, body: Olá Mundo!
    }
}

@RestController vs @Controller — Diferença Importante

@RestController (usado aqui)

  • Retorna dados diretamente no corpo HTTP
  • String retornada = texto da resposta
  • Objeto retornado = JSON na resposta
  • Usado em APIs REST
  • Equivale a @Controller + @ResponseBody em todo método

@Controller (para páginas HTML)

  • Retorna o nome de uma view (arquivo HTML)
  • Usado com Thymeleaf ou JSP
  • String retornada = nome do template a renderizar
  • Usado em aplicações web com interface visual
  • Precisa de @ResponseBody explícito se quiser retornar dados

Como as URLs são construídas com @RequestMapping e @GetMapping

// Regra: URL final = caminho do @RequestMapping + caminho do @GetMapping/@PostMapping etc.

@RequestMapping("medicos")      // → base: /medicos
public class MedicoController {

    @GetMapping                 // → GET  /medicos       (sem caminho adicional)
    public ... listar() { }

    @GetMapping("todos")        // → GET  /medicos/todos
    public ... listarTodos() { }

    @PostMapping                // → POST /medicos
    public void cadastrar() { }

    @PutMapping                 // → PUT  /medicos
    public void atualizar() { }

    @DeleteMapping("/{id}")    // → DELETE /medicos/5  (id é variável na URL)
    public void excluir() { }
}

Os 4 verbos HTTP e suas anotações

Verbo HTTPAnotacao SpringOperacao CRUDCorpo da requisicao?Exemplo de uso
GET@GetMappingRead (Ler)Nao (so URL)Listar medicos, buscar por id
POST@PostMappingCreate (Criar)Sim (JSON no body)Cadastrar medico, agendar consulta
PUT@PutMappingUpdate (Atualizar)Sim (JSON no body)Atualizar nome, telefone
DELETE@DeleteMappingDelete (Excluir)Nao (id na URL)Excluir medico por id

O que acontece da requisicao ate a resposta

1

Cliente envia GET http://localhost:8080/ola

O Insomnia (ou browser) abre uma conexao TCP com a porta 8080 e envia a requisicao HTTP.

2

Tomcat recebe e encaminha ao DispatcherServlet

O servidor embutido recebe a requisicao. O Spring MVC usa o DispatcherServlet como front controller central que roteia para o Controller correto.

3

Handler Mapping encontra o metodo correto

O Spring mapeia GET /ola para o metodo olaMundo() do OlaController com base nas anotacoes.

4

Metodo executa e retorna a String

Java executa return "Ola Mundo!". O @ResponseBody garante que a String vai direto para o corpo da resposta.

5

Tomcat envia a resposta HTTP

Status 200 OK, Content-Type: text/plain, body: "Ola Mundo!". O Insomnia exibe a resposta.

Como testar com Insomnia — passo a passo

1

Inicie a aplicacao

Clique em Run no metodo main da AppApplication. Aguarde a mensagem "Started AppApplication" no console do IntelliJ. O Tomcat sobe na porta 8080.

2

Abra o Insomnia e crie uma Collection

Organize suas requisicoes em uma Collection chamada "VollMed". Isso facilita o reuso durante o desenvolvimento.

3

Crie uma nova requisicao GET

URL: http://localhost:8080/ola — metodo: GET. Nao precisa de body nem headers especiais.

4

Clique em Send e veja a resposta

A resposta deve ser o texto Ola Mundo! com status 200 OK no canto superior direito do Insomnia.

5

Entidade Médico — JPA e Lombok

Uma Entidade e uma classe Java que representa uma tabela no banco de dados. Cada objeto criado a partir dessa classe corresponde a uma linha na tabela. Usamos anotacoes do JPA (Jakarta Persistence API) para fazer esse mapeamento objeto-relacional, e Lombok para eliminar codigo repetitivo.

package com.github.app.model.medico;

// ── Anotações Lombok (geração de código em tempo de compilação) ─────
@Getter
// Gera automaticamente: getNome(), getEmail(), getCrm(), getId()...
// para todos os atributos private da classe. Sem isso, o Spring
// não consegue serializar o objeto para JSON (o Jackson usa getters)

@Setter
// Gera automaticamente: setNome(), setEmail()...
// O JPA precisa dos setters para popular os campos ao ler do banco

@AllArgsConstructor
// Gera um construtor com TODOS os atributos como parâmetro:
// new Medico(id, nome, email, telefone, crm, ativo, especialidade, endereco)
// Usado internamente pelo Lombok e pode ser útil em testes

@NoArgsConstructor
// Gera um construtor SEM argumentos: new Medico()
// OBRIGATÓRIO para o JPA: o Hibernate precisa instanciar a entidade
// sem parâmetros ao ler registros do banco via reflexão Java

@EqualsAndHashCode(of = "id")
// Gera equals() e hashCode() baseados APENAS no campo "id".
// Isso garante que dois objetos Medico com mesmo id são considerados iguais,
// independente dos outros campos. Essencial para coleções (Set, HashMap)

// ── Anotações JPA (mapeamento objeto-relacional) ────────────────────
@Entity
// Marca a classe como uma entidade JPA.
// O Hibernate irá criar/usar uma tabela para esta classe no banco.
// Sem @Entity: o Spring ignora a classe e não cria nenhuma tabela

@Table(name = "medicos")
// Define explicitamente o nome da tabela no banco: "medicos".
// Sem @Table: o Hibernate usa o nome da classe ("Medico") como nome da tabela.
// Convenção: tabelas em minúsculas e plural

public class Medico {

    @Id
    // JPA: indica que este campo é a Chave Primária (PRIMARY KEY) da tabela.
    // Toda entidade JPA DEVE ter exatamente um campo @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // JPA: o banco gera o valor automaticamente usando AUTO_INCREMENT (MySQL)
    // ou SERIAL (PostgreSQL). O H2 também suporta.
    // strategy=IDENTITY: delega a geração ao banco (não ao Hibernate).
    // Sem @GeneratedValue: você precisaria definir o id manualmente

    private Integer id;
    // Tipo Integer (objeto) em vez de int (primitivo) porque pode ser null
    // antes da entidade ser salva — o banco ainda não gerou o id

    private String nome;      // coluna "nome" VARCHAR na tabela
    private String email;     // coluna "email" VARCHAR na tabela
    private String telefone;  // coluna "telefone" VARCHAR na tabela
    private String crm;       // coluna "crm" VARCHAR — identificação profissional

    private Boolean ativo = true;
    // Flag para exclusão lógica: true=ativo, false=desativado.
    // Valor padrão true: novos médicos já são cadastrados como ativos.
    // Permite "excluir" sem apagar do banco (ver Seção 13)

    @Enumerated(EnumType.STRING)
    // JPA: instrui o Hibernate a salvar o TEXTO do enum no banco.
    // Sem isso, o padrão seria EnumType.ORDINAL (salva 0, 1, 2...).
    // EnumType.STRING é muito melhor: salva "CARDIOLOGIA", "ORTOPEDIA".
    // Se você reordenar o enum, os dados não ficam corrompidos

    private Especialidade especialidade;
    // Coluna "especialidade" VARCHAR(255) no banco, ex: "CARDIOLOGIA"

    @Embedded
    // JPA: os campos da classe Endereco são "embutidos" diretamente
    // nesta tabela "medicos". Não cria uma tabela separada de endereços.
    // A classe Endereco deve estar anotada com @Embeddable (ver Seção 12)

    private Endereco endereco;
    // Resultado: as colunas logradouro, bairro, cep, cidade, uf
    // aparecem diretamente na tabela "medicos"

    // ── Construtor de conversão: DTO → Entidade ──────────────────────
    public Medico(DadosCadastroMedico dados) {
        // Este construtor recebe o DTO (que veio do JSON da requisição)
        // e popula os campos da entidade. Separa a camada de entrada
        // da camada de persistência — a entidade nunca é exposta ao cliente

        this.nome = dados.nome();
        // dados.nome() — método acessor de record (sem "get")

        this.email = dados.email();
        this.telefone = dados.telefone();
        this.crm = dados.crm();
        this.especialidade = dados.especialidade();

        this.endereco = new Endereco(dados.endereco());
        // dados.endereco() retorna um DadosCadastroEndereco (outro DTO).
        // O construtor de Endereco converte esse DTO em um objeto Endereco.
    }

    // ── Método de atualização parcial ────────────────────────────────
    public void atualizarInformacoes(DadosAtualizacaoMedico dados) {
        if (dados.nome() != null)     this.nome = dados.nome();
        if (dados.email() != null)    this.email = dados.email();
        if (dados.endereco() != null) this.endereco.atualizarInformacoes(dados.endereco());
        // Verifica null antes de atualizar: se o campo não veio no JSON,
        // ele chegará null no DTO e não sobrescreve o valor existente no banco
    }

    // ── Exclusão lógica ──────────────────────────────────────────────
    public void exclusaoLogica() {
        this.ativo = false;
        // Não deleta do banco — apenas marca como inativo.
        // O @Transactional no Controller detectará a mudança (dirty checking)
        // e gerará UPDATE medicos SET ativo=false WHERE id=?
    }
}

Explicacao detalhada das anotacoes Lombok

AnotacaoO que gera no bytecodePor que voce precisa
@GettergetNome(), getEmail(), getCrm(), getId(), getAtivo(), getEspecialidade(), getEndereco()Jackson usa getters para serializar para JSON. JPA usa para ler campos.
@SettersetNome(), setEmail(), etc.JPA/Hibernate usa setters para popular entidade ao ler do banco. Necessario para dirty checking.
@AllArgsConstructorMedico(Integer id, String nome, String email, ...)Requerido quando ha @NoArgsConstructor para que o compilador nao elimine construtores necessarios.
@NoArgsConstructorMedico() { }OBRIGATORIO para JPA. O Hibernate usa reflexao Java para criar instancias — precisa do construtor sem parametros.
@EqualsAndHashCode(of="id")equals() e hashCode() baseados apenas no campo idPermite comparar dois objetos Medico corretamente em Sets e Maps. Sem isso, a comparacao usa todos os campos.

Anotacoes JPA e o que fazem no banco

AnotacaoEfeito no bancoO que quebra sem ela
@EntityCria a tabela no bancoNenhuma tabela e criada. JpaRepository nao funciona.
@Table(name="medicos")Define o nome da tabelaTabela seria chamada "Medico" (nome da classe)
@IdDefine a Primary KeyErro: toda entidade JPA precisa de @Id
@GeneratedValue(IDENTITY)AUTO_INCREMENT na coluna idVoce precisaria fornecer o id manualmente em cada save()
@Enumerated(STRING)Salva "CARDIOLOGIA" em vez de 0Salva o ordinal (0, 1, 2). Reordenar o enum corrompe dados.
@EmbeddedColunas de Endereco entram na tabela medicosErro: campo ignorado sem mapeamento JPA

O enum Especialidade

package com.github.app.model.medico;

public enum Especialidade {
    ORTOPEDIA,    // tratamento de ossos, articulações e músculos
    CARDIOLOGIA,  // doenças do coração e sistema circulatório
    GINECOLOGIA,  // saúde da mulher
    DERMATOLOGIA; // doenças da pele
}
// Um enum é um tipo especial que define um conjunto fixo de valores possíveis.
// No banco (com @Enumerated(STRING)), será salvo como texto: "CARDIOLOGIA".
// No JSON de entrada, o cliente deve enviar exatamente a string: "CARDIOLOGIA"
Por que @NoArgsConstructor e obrigatorio no JPA? O JPA (Hibernate) usa reflexao Java para criar instancias de entidades ao ler linhas do banco. O processo e: (1) cria um objeto vazio com new Medico(), (2) popula cada campo com o valor do banco usando os setters. Se nao houver construtor sem parametros, o passo 1 falha com InstantiationException e nenhuma consulta ao banco funcionara.
GenerationType.IDENTITY vs outras estrategias
  • IDENTITY: delega ao banco (AUTO_INCREMENT). Recomendado para MySQL/H2.
  • SEQUENCE: usa uma sequence do banco (melhor para PostgreSQL — permite batch insert).
  • AUTO: Spring escolhe automaticamente baseado no banco detectado.
  • TABLE: usa uma tabela auxiliar para gerar ids. Mais lento, raramente usado.
6

DTO com Records — DadosCadastroMedico

Um DTO (Data Transfer Object) e um objeto simples que carrega dados entre a API e a aplicacao. Usamos DTOs em vez de expor a entidade diretamente porque:

Motivo 1

Seguranca

A entidade tem campos que o usuario nao deve enviar: id, ativo. O DTO controla exatamente o que entra.

Motivo 2

Flexibilidade

Voce pode ter DTOs diferentes para entrada (POST), saida (GET) e atualizacao (PUT) — cada um com campos diferentes.

Motivo 3

Desacoplamento

Mudancas na estrutura do banco nao afetam o contrato da API — voce so ajusta o DTO, nao o que o cliente recebe.

Motivo 4

Validacao

As regras de validacao (@NotBlank, @Email) ficam no DTO — nao na entidade. Isso separa as responsabilidades.

O que e um record em Java?

Introduzido no Java 16, o record e uma forma compacta de criar classes imutaveis que armazenam dados. Um record gera automaticamente: construtor canônico, metodos acessores, equals(), hashCode() e toString().

Com record (Java 16+) — forma usada no projeto

  • 3 linhas para um DTO completo
  • Imutavel por padrao
  • Acessores: dados.nome() (sem "get")
  • equals/hashCode/toString automaticos
  • Construtor canonico automatico

Com classe normal (Java antigo)

  • 30+ linhas de codigo boilerplate
  • Precisa de getters/setters manuais
  • Acessores: dados.getNome()
  • equals/hashCode manuais (ou Lombok)
  • Construtor manual
// ── Com RECORD (forma moderna — usada no projeto) ───────────────────
public record DadosCadastroMedico(
    String nome,
    String email,
    String telefone,
    String crm,
    Especialidade especialidade,
    DadosCadastroEndereco endereco
) { }
// Para acessar: dados.nome(), dados.email(), dados.crm()
//              SEM "get" na frente — essa é a sintaxe do record!

// ── Com CLASSE NORMAL (equivalente, mais verboso) ───────────────────
public class DadosCadastroMedicoClasseNormal {
    private String nome;
    private String email;
    private String telefone;
    private String crm;
    private Especialidade especialidade;
    private DadosCadastroEndereco endereco;

    public DadosCadastroMedicoClasseNormal(String nome, String email,
            String telefone, String crm, Especialidade especialidade,
            DadosCadastroEndereco endereco) {
        this.nome = nome; this.email = email; ...
    }
    public String getNome() { return nome; }
    public String getEmail() { return email; }
    // ... mais 8 getters, equals(), hashCode(), toString() ...
}

DadosCadastroMedico — linha por linha

package com.github.app.model.medico;

public record DadosCadastroMedico(
    // Campos que o cliente DEVE enviar no JSON ao fazer POST /medicos

    String nome,
    // "nome": "Dr. Joao Silva" — nome completo do médico

    String email,
    // "email": "joao@clinic.com" — e-mail de contato

    String telefone,
    // "telefone": "11999998888" — campo opcional (sem validação)

    String crm,
    // "crm": "123456" — registro profissional do médico

    Especialidade especialidade,
    // "especialidade": "CARDIOLOGIA" — deve ser um dos valores do enum Especialidade
    // O Jackson converte automaticamente a String para o enum correspondente

    DadosCadastroEndereco endereco
    // "endereco": { ... } — outro DTO aninhado com os campos de endereço
    // O Jackson converte o objeto JSON para um DadosCadastroEndereco automaticamente
) { }
// O corpo vazio {} significa: sem métodos customizados (apenas o que o record gera)

DadosCadastroEndereco — o DTO aninhado

public record DadosCadastroEndereco(
    String logradouro,  // nome da rua: "Rua das Flores"
    String bairro,      // bairro: "Centro"
    String cep,         // CEP: "01001000" (sem traço)
    String complemento, // complemento: "Ap 42" — opcional
    String cidade,      // cidade: "Sao Paulo"
    String uf           // estado: "SP" — sigla de 2 letras
) { }

JSON completo que o cliente precisa enviar

// POST http://localhost:8080/medicos
// Content-Type: application/json
{
  "nome":          "Dr. Joao Silva",
  "email":         "joao@clinic.com",
  "telefone":      "11999998888",
  "crm":           "123456",
  "especialidade": "CARDIOLOGIA",
  "endereco": {
    "logradouro":  "Rua das Flores",
    "bairro":      "Centro",
    "cep":         "01001000",
    "complemento": "Sala 202",
    "cidade":      "Sao Paulo",
    "uf":          "SP"
  }
}
// O Spring/Jackson desserializa esse JSON para um DadosCadastroMedico automaticamente.
// O campo "endereco" vira um DadosCadastroEndereco aninhado.

Como o Jackson faz a deserializacao

1

POST /medicos chega com JSON no corpo

O Tomcat recebe a requisicao. O Content-Type deve ser application/json.

2

Jackson le o @RequestBody

O parametro @RequestBody DadosCadastroMedico dados instrui o Jackson a converter o JSON para um objeto DadosCadastroMedico.

3

Jackson popula cada campo pelo nome

O campo "nome" do JSON vai para dados.nome(). O campo "especialidade": "CARDIOLOGIA" e convertido para Especialidade.CARDIOLOGIA.

4

Objeto DadosCadastroMedico esta pronto

O metodo cadastrar(dados) recebe o objeto completamente populado, pronto para uso.

Acessores de record: dados.nome() nao dados.getNome() Em um record, os metodos acessores tem o mesmo nome do campo, sem o prefixo "get". Isso e diferente de uma classe normal com Lombok (@Getter). Quando voce ver dados.nome() no codigo, saiba que dados e um record. Quando ver medico.getNome(), e uma entidade com @Getter do Lombok.
7

Bean Validation — Validando os Dados

O Bean Validation permite adicionar regras de validacao diretamente nos campos do DTO usando anotacoes. Se o dado recebido nao atender a regra, o Spring retorna automaticamente um erro 400 Bad Request com detalhes do problema — sem precisar escrever codigo de validacao manual.

DadosCadastroMedico com validacoes

package com.github.app.model.medico;

import jakarta.validation.Valid;
import jakarta.validation.constraints.*;

public record DadosCadastroMedico(

    @NotBlank
    // Valida: não nulo E não vazio E não apenas espaços em branco.
    // Exemplos que FALHAM: null, "", "   "
    // Exemplos que PASSAM: "Dr. João", "a"
    String nome,

    @Email
    // Valida o formato do e-mail: deve ter @ e domínio.
    // Exemplos que FALHAM: "joao", "joao@", "@clinic.com"
    // Exemplos que PASSAM: "joao@clinic.com"
    @NotBlank
    // Combinando duas anotações: deve ser não-vazio E formato de e-mail
    String email,

    String telefone,
    // Sem anotação = campo opcional. null é aceito. Pode ser vazio.

    String crm,
    // Também opcional neste projeto (poderia ter @NotBlank em produção)

    @NotNull
    // Valida: não pode ser null. Diferente de @NotBlank: aceita string vazia.
    // Usado para tipos que não são String: enums, objetos, números.
    // Exemplos que FALHAM: null, campo ausente no JSON
    // Exemplos que PASSAM: "CARDIOLOGIA", qualquer valor do enum
    Especialidade especialidade,

    @NotNull
    @Valid
    // @Valid é ESSENCIAL: propaga a validação para DENTRO do objeto DadosCadastroEndereco.
    // Sem @Valid, as anotações @NotBlank dentro do DadosCadastroEndereco são IGNORADAS.
    // Pense assim: @Valid diz "valide também o objeto aninhado, não só se é null"
    DadosCadastroEndereco endereco

) { }

DadosCadastroEndereco com validacoes

public record DadosCadastroEndereco(

    @NotBlank
    String logradouro,  // obrigatório: nome da rua

    @NotBlank
    String bairro,      // obrigatório: bairro

    @NotBlank
    String cep,         // obrigatório: CEP (poderia ter @Pattern para validar formato)

    String complemento, // opcional: apartamento, sala, etc.

    @NotBlank
    String cidade,      // obrigatório: cidade

    @NotBlank
    String uf           // obrigatório: estado (sigla)

) { }

ATENCAO: @Valid tambem no Controller!

// ── SEM @Valid no Controller (ERRADO — validações ignoradas) ────────
@PostMapping
public void cadastrar(@RequestBody DadosCadastroMedico dados) {
    repository.save(new Medico(dados));
    // ⚠️ Sem @Valid: dados com nome="" ou email="invalido" seriam aceitos!
}

// ── COM @Valid no Controller (CORRETO — validações ativadas) ────────
@PostMapping
public void cadastrar(@RequestBody @Valid DadosCadastroMedico dados) {
    // ✅ @Valid ANTES do parâmetro: o Spring valida o DTO antes de executar o método.
    // Se qualquer campo falhar a validação → 400 Bad Request automático.
    // O método não executa — nenhum dado inválido chega ao banco.
    repository.save(new Medico(dados));
}

Diferenca entre @NotBlank, @NotNull e @NotEmpty

Anotacaonull""" ""abc"Melhor para
@NotNullFALHApassapassapassaEnums, objetos, numeros — qualquer tipo
@NotEmptyFALHAFALHApassapassaStrings e colecoes que nao podem ser vazias
@NotBlankFALHAFALHAFALHApassaStrings — a mais restritiva, mais usada em campos de texto

Tabela completa de anotacoes de validacao

AnotacaoO que validaTipo de campoExemplo
@NotBlankNao nulo, nao vazio, nao so espacosStringnome, email, cep
@NotNullNao nulo (aceita vazio)Qualquerespecialidade (enum), endereco
@NotEmptyNao nulo e nao vazioString, List, Setlista de itens
@EmailFormato valido de e-mailStringemail
@Min(n)Valor numerico minimo nint, long, BigDecimal@Min(18) idade
@Max(n)Valor numerico maximo nint, long, BigDecimal@Max(120) idade
@Size(min,max)Tamanho entre min e maxString, colecoes@Size(min=3,max=50)
@Pattern(regexp)Expressao regularString@Pattern(regexp="\\d{8}") cep
@PastData no passadoLocalDate, LocalDateTimedata de nascimento
@FutureData no futuroLocalDate, LocalDateTimedata de consulta
@ValidPropaga validacao para objeto aninhadoObjetosendereco, itens de lista

O que acontece quando a validacao falha

// Requisição enviada com nome vazio:
{
  "nome": "",
  "email": "invalido-sem-arroba",
  "especialidade": "CARDIOLOGIA",
  "endereco": { ... }
}

// Resposta automática do Spring: 400 Bad Request
{
  "status": 400,
  "errors": [
    { "field": "nome",  "message": "must not be blank" },
    { "field": "email", "message": "must be a well-formed email address" }
  ]
}
// O método cadastrar() NÃO é executado. Nada é salvo no banco.
Dois @Valid necessarios para validar endereco aninhado Para que as validacoes dentro de DadosCadastroEndereco funcionem, voce precisa de @Valid em dois lugares: (1) no campo endereco dentro do record DadosCadastroMedico, e (2) no parametro do metodo no Controller: @RequestBody @Valid DadosCadastroMedico dados. Faltando qualquer um deles, as validacoes do endereco sao silenciosamente ignoradas.
8

Repository — Acesso ao Banco de Dados

O Repository e a interface responsavel por acessar o banco de dados. Com o Spring Data JPA, voce nao precisa escrever SQL — basta criar uma interface que herda de JpaRepository e o Spring gera todos os metodos de consulta automaticamente, incluindo implementacoes completas com SQL otimizado.

package com.github.app.model.medico;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
// Marca a interface como um componente de acesso a dados.
// Tecnicamente opcional — o Spring já detecta interfaces que herdam JpaRepository.
// Mas é boa prática manter: documenta a intenção e habilita tradução de exceções
// (ex: DataAccessException em vez de HibernateException)

public interface MedicoRepository extends JpaRepository<Medico, Integer> {
// ↑ Interface, não classe — o Spring cria a implementação em tempo de execução!
//
// JpaRepository recebe dois tipos genéricos:
//   Medico   → o tipo da entidade que este repository gerencia
//   Integer  → o tipo da chave primária (@Id) da entidade Medico
//
// Ao herdar JpaRepository, você ganha GRATUITAMENTE:
//   save(), findAll(), findById(), deleteById(), getReferenceById(),
//   existsById(), count(), findAll(Pageable), saveAll()...
//
// O Spring Data JPA usa um Proxy dinâmico para implementar esta interface.
// Você nunca escreve "new MedicoRepositoryImpl()" — o Spring faz isso.

    // Corpo vazio — tudo que precisamos já está em JpaRepository!
    // Poderia adicionar métodos customizados aqui. Ex:
    // List<Medico> findByEspecialidade(Especialidade especialidade);
    // O Spring gera automaticamente o SQL: SELECT * FROM medicos WHERE especialidade = ?
}

Genericos JpaRepository: <Medico, Integer>

O que significam os genericos?
  • Primeiro tipo (Medico): diz ao JpaRepository qual entidade ele gerencia. O Spring saberá que findAll() retorna List<Medico>, que save() aceita um Medico etc.
  • Segundo tipo (Integer): diz qual e o tipo da chave primaria. O findById() aceitara um Integer. O deleteById() tambem.
  • Para PacienteRepository seria JpaRepository<Paciente, Integer>. Para ConsultaRepository, JpaRepository<Consulta, Integer>.

Todos os metodos gerados automaticamente

Metodo JavaSQL geradoRetornoUso no projeto
save(medico)INSERT ou UPDATEMedico (com id preenchido)cadastrar(), agendar()
findAll()SELECT * FROM medicosList<Medico>listarTodos(), listar()
findAll(pageable)SELECT * LIMIT ? OFFSET ?Page<Medico>listarPorPagina()
findById(id)SELECT * WHERE id = ?Optional<Medico>quando pode nao existir
getReferenceById(id)Cria proxy — busca ao acessarMedico (proxy)atualizar(), excluir()
deleteById(id)DELETE WHERE id = ?voidexcluir()
existsById(id)SELECT 1 WHERE id = ?booleanverificar existencia
count()SELECT COUNT(*)longtotal de registros
saveAll(list)Multiplos INSERTList<Medico>importacao em massa

getReferenceById() vs findById() — diferenca critica

getReferenceById(id)

  • Retorna um proxy (objeto lazy)
  • Nao faz SELECT imediatamente
  • O SELECT so ocorre quando voce acessa um campo
  • Mais eficiente para atualizar e excluir
  • Lanca EntityNotFoundException se o id nao existir (ao acessar)

findById(id)

  • Faz SELECT imediatamente
  • Retorna Optional<Medico>
  • Voce pode verificar se existe: .isPresent()
  • Mais seguro quando a existencia e incerta
  • Use quando precisar verificar se o registro existe antes de agir

@Autowired — Injecao de Dependencia

@RestController
public class MedicoController {

    @Autowired
    // Injeção de Dependência: em vez de você criar o objeto:
    //     MedicoRepository repo = new MedicoRepository(); // ERRADO — é interface!
    // O Spring cria a implementação e "injeta" aqui automaticamente.
    //
    // Como funciona:
    // 1. @ComponentScan encontra MedicoRepository (marcado com @Repository)
    // 2. Spring cria a implementação via proxy dinâmico
    // 3. Spring detecta @Autowired no Controller
    // 4. Spring injeta a instância criada automaticamente
    //
    // Benefício: baixo acoplamento. Se trocar MedicoRepository por outro,
    // o Controller não precisa mudar nada.
    private MedicoRepository repository;
}
O Spring Data JPA gera o SQL — voce nao precisa escrever Quando voce chama repository.findAll(), o Hibernate gera SELECT m.id, m.nome, m.email... FROM medicos m. Quando chama repository.save(medico), ele gera INSERT INTO medicos (nome, email, crm...) VALUES (?, ?, ?...) com os valores do objeto Java. Voce nunca escreve SQL manual — apenas chama metodos Java.
9

MedicoController — CRUD Completo

Agora que temos Entidade, DTOs e Repository, montamos o Controller com todas as operacoes CRUD. Cada metodo responde a um verbo HTTP diferente e usa um DTO diferente.

Estrutura geral do Controller

@RestController          // registra como controller que retorna dados JSON
@RequestMapping("medicos") // todos os endpoints começam com /medicos
public class MedicoController {

    @Autowired
    private MedicoRepository repository;
    // Spring injeta automaticamente a implementação do repository
}

POST /medicos — Cadastrar medico

@PostMapping
// Responde a: POST http://localhost:8080/medicos
// Usado para CRIAR um novo recurso

@Transactional
// Abre uma transação de banco de dados antes de executar o método.
// Se o método terminar sem exceção → COMMIT (confirmação no banco).
// Se ocorrer uma exceção → ROLLBACK (desfaz tudo).
// Obrigatório no cadastrar: garante que o INSERT seja confirmado.

public void cadastrar(@RequestBody @Valid DadosCadastroMedico dados) {
    // @RequestBody: diz ao Spring para ler o JSON do corpo da requisição
    //              e desserializar para DadosCadastroMedico
    // @Valid:      ativa a Bean Validation — campos @NotBlank etc. são verificados

    repository.save(new Medico(dados));
    // new Medico(dados): chama o construtor especial que converte DTO → Entidade
    // repository.save(): gera INSERT INTO medicos (nome, email...) VALUES (?, ?...)
}
// JSON para teste no Insomnia:
// POST /medicos
// { "nome": "Dr. Ana", "email": "ana@clinic.com", "crm": "654321",
//   "especialidade": "GINECOLOGIA",
//   "endereco": { "logradouro": "Av. Brasil", "bairro": "Centro",
//                 "cep": "01001000", "cidade": "SP", "uf": "SP" } }

GET /medicos/todos — Listar todos (retorna entidade bruta)

@GetMapping("todos")
// Responde a: GET http://localhost:8080/medicos/todos

public List<Medico> listarTodos() {
    return repository.findAll();
    // ⚠️ Retorna a entidade Medico diretamente — má prática!
    // Problemas:
    // 1. Expõe campos internos (ativo) ao cliente
    // 2. Se houver @ManyToOne, pode causar LazyInitializationException
    // 3. Serialização recursiva infinita em relacionamentos bidirecionais
    // Use apenas para depuração. Em produção, use sempre um DTO de listagem.
}

GET /medicos/listar — Listar com Stream e DTO

@GetMapping("listar")
// Responde a: GET http://localhost:8080/medicos/listar
// Sem paginação — retorna todos de uma vez (ruim para grandes volumes)

public List<DadosListagemMedico> listar() {
    return repository.findAll()
                      .stream()
                      // .stream() → transforma List<Medico> em um fluxo de dados
                      .map(DadosListagemMedico::new)
                      // .map() → para cada Medico no fluxo, cria um DadosListagemMedico
                      // DadosListagemMedico::new é uma referência ao construtor do record
                      .toList();
                      // .toList() → coleta todos os elementos em uma List imutável
}
// Resultado: List<DadosListagemMedico> — só id, nome, email, crm, especialidade.
// Sem endereço completo, sem campo ativo — apenas o que o cliente precisa ver.

GET /medicos — Listar com paginacao

@GetMapping
// Responde a: GET http://localhost:8080/medicos
// Aceita parâmetros: ?page=0&size=10&sort=nome,asc

public Page<DadosListagemMedico> listarPorPagina(Pageable paginacao) {
    // Pageable: o Spring preenche automaticamente com os parâmetros da URL.
    //   GET /medicos?page=1&size=5 → paginacao = {page:1, size:5}
    //   GET /medicos?sort=nome,desc → ordena por nome decrescente
    // Se não passar parâmetros: page=0, size=20 (padrões do Spring)

    return repository.findAll(paginacao)
                      // findAll(Pageable) gera: SELECT * FROM medicos LIMIT ? OFFSET ?
                      .map(DadosListagemMedico::new);
                      // Page tem .map() nativo — não precisa de .stream()
                      // Converte cada Medico da página em DadosListagemMedico
}
// Resposta JSON inclui: content[], totalElements, totalPages, size, number...

PUT /medicos — Atualizar medico

@PutMapping
// Responde a: PUT http://localhost:8080/medicos
// Usado para ATUALIZAR um recurso existente

@Transactional
// ESSENCIAL aqui: sem @Transactional, o dirty checking não funciona.
// O Hibernate precisa de uma transação ativa para detectar mudanças
// e gerar o UPDATE automaticamente

public void atualizar(@RequestBody DadosAtualizacaoMedico dados) {

    var medico = repository.getReferenceById(dados.id());
    // var: palavra-chave do Java 10+ para inferência de tipo.
    //      var medico = ... equivale a Medico medico = ...
    //      O compilador Java infere o tipo pelo lado direito.
    //
    // getReferenceById(dados.id()): retorna um proxy do Medico com o id informado.
    //   Não faz SELECT ainda — o SELECT ocorre ao acessar os campos.
    //   Se o id não existir, lança EntityNotFoundException ao acessar.

    medico.atualizarInformacoes(dados);
    // Chama o método da entidade que atualiza os campos com null-check.
    // O Hibernate monitora o objeto medico dentro da transação (@Transactional).
    // Ao detectar que os campos mudaram (dirty checking), gera automaticamente:
    // UPDATE medicos SET nome=?, email=? WHERE id=?
    // NÃO precisa chamar repository.save() aqui!
}
// JSON para teste: PUT /medicos
// { "id": 1, "nome": "Dr. Joao Atualizado" }
// Só envie os campos que quer alterar — null não sobrescreve

DELETE /medicos/{id} — Excluir medico

@DeleteMapping("/{id}")
// Responde a: DELETE http://localhost:8080/medicos/5
// {id} é uma variável na URL — capturada pelo @PathVariable

@Transactional
public void excluir(@PathVariable Integer id) {
    // @PathVariable: extrai o {id} da URL e injeta no parâmetro.
    // DELETE /medicos/5 → id = 5
    // O nome {id} na URL DEVE ser igual ao nome do parâmetro (id)

    repository.deleteById(id);
    // Gera: DELETE FROM medicos WHERE id = ?
    // Exclusão física — o registro é removido permanentemente do banco.
    // Ver Seção 13 para exclusão lógica (alternativa recomendada)
}
// Teste no Insomnia: DELETE http://localhost:8080/medicos/5

O que e @Transactional — explicacao profunda

@Transactional na pratica — dirty checking
  • Uma transacao e uma unidade atomica de trabalho no banco: ou tudo acontece, ou nada acontece.
  • O dirty checking e um mecanismo do Hibernate: quando uma entidade e carregada dentro de uma transacao (@Transactional), o Hibernate tira um "snapshot" do estado inicial. Ao final da transacao, ele compara o estado atual com o snapshot e gera automaticamente os UPDATE necessarios.
  • Por isso no atualizar() nao chamamos save(): o Hibernate detecta que medico.nome mudou e gera o UPDATE sozinho.
  • Sem @Transactional: o Hibernate nao gerencia o ciclo de vida da entidade, o dirty checking nao funciona, e voce precisaria chamar save() explicitamente.

Resumo dos endpoints do MedicoController

Metodo HTTPURLMetodo JavaDescricao
POST/medicoscadastrar()Cadastra novo medico
GET/medicos/todoslistarTodos()Lista todos (entidade bruta)
GET/medicos/listarlistar()Lista com DTO (sem paginacao)
GET/medicos?page=0&size=10listarPorPagina()Lista paginada com DTO
PUT/medicosatualizar()Atualiza dados (id no body)
DELETE/medicos/{id}excluir()Exclui por id na URL
10

DadosListagemMedico — Stream API explicada

O DadosListagemMedico e o DTO retornado nos endpoints de listagem GET. Ele contem apenas os campos relevantes para exibicao — sem endereço completo, sem campo ativo, sem informacoes desnecessarias. A conversao de List<Medico> para List<DadosListagemMedico> usa a Stream API do Java.

DadosListagemMedico — linha por linha

public record DadosListagemMedico(
    Integer id,               // id do médico — necessário para operações futuras (PUT, DELETE)
    String nome,              // nome para exibição na lista
    String email,             // contato
    String crm,               // identificação profissional
    Especialidade especialidade // filtro/agrupamento por especialidade
    // ⚠️ Sem: telefone, ativo, endereço completo — não necessários na listagem
) {
    // Construtor de conversão: recebe uma Entidade Medico e popula o record
    public DadosListagemMedico(Medico medico) {
        this(
            medico.getId(),
            medico.getNome(),
            medico.getEmail(),
            medico.getCrm(),
            medico.getEspecialidade()
        );
        // this(...) chama o construtor canônico do record com todos os campos.
        // medico.getNome() usa o getter gerado pelo Lombok @Getter na entidade.
    }
}

Stream API — o que e um Stream?

Um Stream e um fluxo de dados que pode ser processado de forma declarativa (descrevendo O QUE fazer, nao COMO). E como uma esteira industrial: os dados entram de um lado, passam por operacoes intermediarias (map, filter, sorted) e saem do outro lado coletados (toList, toSet, toMap).

// ── Versão COM Stream API (forma do projeto) ────────────────────────
return repository.findAll()          // 1. List<Medico> do banco
                  .stream()           // 2. transforma em Stream<Medico>
                  .map(DadosListagemMedico::new) // 3. converte cada Medico em DadosListagemMedico
                  .toList();           // 4. coleta em List<DadosListagemMedico>

// ── Versão SEM Stream (equivalente, mais verboso) ────────────────────
List<Medico> medicos = repository.findAll();
List<DadosListagemMedico> resultado = new ArrayList<>();
for (Medico medico : medicos) {
    DadosListagemMedico dto = new DadosListagemMedico(medico);
    resultado.add(dto);
}
return resultado;
// As duas versões fazem EXATAMENTE a mesma coisa.
// A versão Stream é mais legível e menos propensa a erros.

Cada operacao do Stream explicada

Passo 1

.stream()

Transforma a List<Medico> em um Stream<Medico>. Nenhum dado e processado ainda — e so uma preparacao para as operacoes seguintes. Streams sao lazy: so processam quando uma operacao terminal e chamada.

Passo 2

.map(funcao)

Operacao intermediaria: transforma cada elemento do stream aplicando a funcao. map(DadosListagemMedico::new) converte cada Medico em DadosListagemMedico. O stream resultante e Stream<DadosListagemMedico>.

Passo 3

.toList()

Operacao terminal: aciona o processamento de tudo e coleta os elementos em uma List imutavel. Sem uma operacao terminal, o stream nunca executa. Disponivel desde Java 16.

O que e uma referencia de metodo (::new)?

// Referência de método: uma forma concisa de referenciar um método/construtor existente

// DadosListagemMedico::new  →  referência ao CONSTRUTOR do record
// Equivale à lambda: medico -> new DadosListagemMedico(medico)

.map(DadosListagemMedico::new)
// O mesmo que:
.map(medico -> new DadosListagemMedico(medico))

// Outros exemplos de referências de método:
// String::toUpperCase    → s -> s.toUpperCase()
// System.out::println   → s -> System.out.println(s)
// Integer::parseInt     → s -> Integer.parseInt(s)

Outras operacoes do Stream que voce pode usar

// filter() — filtra elementos que atendem a condição
repository.findAll().stream()
    .filter(m -> m.getAtivo())  // só médicos ativos
    .map(DadosListagemMedico::new)
    .toList();

// sorted() — ordena
.sorted((a, b) -> a.getNome().compareTo(b.getNome()))

// collect(Collectors.toList()) — alternativa mais antiga a .toList()
.collect(Collectors.toList())  // retorna List mutável
// .toList() retorna List imutável (Java 16+)

Exemplo de resposta JSON da listagem

// GET http://localhost:8080/medicos/listar
[
  {
    "id": 1,
    "nome": "Dr. Joao Silva",
    "email": "joao@clinic.com",
    "crm": "123456",
    "especialidade": "CARDIOLOGIA"
  },
  {
    "id": 2,
    "nome": "Dra. Ana Costa",
    "email": "ana@clinic.com",
    "crm": "654321",
    "especialidade": "GINECOLOGIA"
  }
]
// Campos ausentes: telefone, ativo, endereço — protegidos pelo DTO
11

DadosAtualizacaoMedico — Atualizacao Parcial

A atualizacao parcial permite que o cliente envie apenas os campos que deseja alterar. Os campos que nao vierem no JSON chegam como null no DTO e sao ignorados — sem sobrescrever dados existentes no banco.

DadosAtualizacaoMedico — o DTO do PUT

public record DadosAtualizacaoMedico(

    Integer id,
    // O id é OBRIGATÓRIO: o Controller precisa saber qual médico atualizar.
    // Diferente do cadastro (POST), o id vem no BODY do PUT, não na URL.
    // Alternativa: usar PUT /medicos/{id} com @PathVariable (igualmente válido)

    String nome,
    // Opcional: se null no JSON, o campo nome do médico não é alterado

    String email,
    // Opcional: pode atualizar só o email sem tocar no nome

    DadosCadastroEndereco endereco
    // Opcional: se null, o endereço não é tocado.
    // Se enviado, os campos não-null dentro do endereco são atualizados.
    // Usa o mesmo DTO de cadastro (DadosCadastroEndereco) para simplificar.
    // Nota: telefone e crm NÃO aparecem aqui — não são atualizáveis neste projeto

) { }

atualizarInformacoes() — verificacao de null em cada campo

// Na entidade Medico.java:
public void atualizarInformacoes(DadosAtualizacaoMedico dados) {

    if (dados.nome() != null) {
        this.nome = dados.nome();
        // Só atualiza o nome se ele veio no JSON.
        // Se "nome" não está no JSON → dados.nome() retorna null → if falso → campo ignorado
        // Se "nome": "Dr. Novo" está no JSON → dados.nome() retorna "Dr. Novo" → atualiza
    }

    if (dados.email() != null) {
        this.email = dados.email();
        // Mesma lógica: só atualiza se o email vier no JSON
    }

    if (dados.endereco() != null) {
        this.endereco.atualizarInformacoes(dados.endereco());
        // Se o objeto endereco vier no JSON, delega a atualização para
        // o método atualizarInformacoes() da classe Endereco,
        // que fará null-check campo por campo dentro do endereço também
    }
    // Ao final: o @Transactional no Controller detecta quais campos mudaram
    // (dirty checking) e gera apenas: UPDATE medicos SET nome=? WHERE id=?
    // Campos não alterados NÃO aparecem no UPDATE
}

Exemplos de JSON para atualizacao parcial

// ── Atualizar apenas o nome ─────────────────────────────────────────
// PUT /medicos
{ "id": 1, "nome": "Dr. Joao Atualizado" }
// SQL gerado: UPDATE medicos SET nome='Dr. Joao Atualizado' WHERE id=1
// email e endereco: null no DTO → não alterados

// ── Atualizar apenas o endereço ─────────────────────────────────────
// PUT /medicos
{
  "id": 1,
  "endereco": {
    "cidade": "Rio de Janeiro",
    "uf": "RJ"
  }
}
// SQL: UPDATE medicos SET cidade='Rio de Janeiro', uf='RJ' WHERE id=1
// Outros campos do endereço: null → não alterados

// ── Atualizar tudo ──────────────────────────────────────────────────
// PUT /medicos
{
  "id": 1,
  "nome": "Dr. Joao",
  "email": "novoemail@clinic.com",
  "endereco": {
    "logradouro": "Rua Nova", "bairro": "Jardins",
    "cep": "01400000", "cidade": "Sao Paulo", "uf": "SP"
  }
}
// SQL: UPDATE medicos SET nome=?, email=?, logradouro=?, bairro=?,
//      cep=?, cidade=?, uf=? WHERE id=1

Como o dirty checking funciona no PUT

1

@Transactional abre uma transacao

Antes de executar o metodo atualizar(), o Spring abre uma transacao de banco. O Hibernate comeca a monitorar os objetos carregados.

2

getReferenceById() carrega o Medico

O Hibernate faz SELECT do medico com o id informado e tira um snapshot do estado original: nome="Dr. Joao Antigo", email="antigo@clinic.com".

3

atualizarInformacoes() modifica os campos

O metodo altera this.nome = "Dr. Joao Novo". O objeto Java foi modificado. O banco ainda tem o valor antigo.

4

Fim do metodo: @Transactional faz flush

O Hibernate compara o estado atual com o snapshot. Detecta que nome mudou. Gera automaticamente: UPDATE medicos SET nome=? WHERE id=?. Faz COMMIT.

Por que verificar != null antes de atualizar? Quando o cliente envia PUT /medicos com {"id": 1, "nome": "Dr. Novo"}, o campo email nao esta no JSON. O Jackson preenche o campo email do record com null. Se voce fizesse this.email = dados.email() sem verificar null, o email do banco seria sobrescrito com null — apagando um dado que nao devia mudar. O null-check evita esse problema.
12

Endereco — @Embeddable e @Embedded

O endereco poderia ser uma tabela separada (com chave estrangeira). Mas no VollMed, ele e embutido diretamente nas tabelas de medicos e pacientes. As anotacoes @Embeddable e @Embedded fazem isso sem necessidade de JOIN.

Endereco.java — linha por linha

package com.github.app.model.endereco;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
// Mesmas anotacoes Lombok da entidade: getters, setters, construtores.
// @NoArgsConstructor é necessário porque @Embeddable também é instanciado
// pelo JPA via reflexão ao ler do banco

@Embeddable
// JPA: marca esta classe como "embutível".
// Isso significa que os campos desta classe podem ser incorporados
// diretamente na tabela de qualquer entidade que usar @Embedded.
// Endereco NÃO é uma entidade — não tem @Entity, não tem @Id,
// não tem tabela própria no banco de dados.

public class Endereco {

    private String logradouro; // → coluna "logradouro" na tabela pai (medicos ou pacientes)
    private String bairro;     // → coluna "bairro"
    private String cep;        // → coluna "cep"
    private String complemento;// → coluna "complemento" (pode ser null)
    private String cidade;     // → coluna "cidade"
    private String uf;         // → coluna "uf"

    // ── Construtor de conversão: DTO → Embeddable ────────────────────
    public Endereco(DadosCadastroEndereco dados) {
        this.logradouro = dados.logradouro();
        this.bairro = dados.bairro();
        this.cep = dados.cep();
        this.complemento = dados.complemento();
        this.cidade = dados.cidade();
        this.uf = dados.uf();
        // Converte DadosCadastroEndereco (DTO imutável) em Endereco (objeto mutável)
    }

    // ── Atualizacao parcial do endereco ─────────────────────────────
    public void atualizarInformacoes(DadosCadastroEndereco dados) {
        if (dados.logradouro() != null) this.logradouro = dados.logradouro();
        if (dados.bairro() != null)     this.bairro = dados.bairro();
        if (dados.cep() != null)       this.cep = dados.cep();
        if (dados.complemento() != null) this.complemento = dados.complemento();
        if (dados.cidade() != null)   this.cidade = dados.cidade();
        if (dados.uf() != null)       this.uf = dados.uf();
        // Null-check campo por campo: permite atualizar só cidade sem tocar CEP, bairro etc.
    }
}

Como @Embedded usa a classe Endereco em Medico

@Entity
@Table(name = "medicos")
public class Medico {
    // ... outros campos (id, nome, email, crm, ativo, especialidade) ...

    @Embedded
    // JPA: inclui todos os campos da classe Endereco (@Embeddable)
    // diretamente nesta tabela "medicos".
    // Resultado: as colunas logradouro, bairro, cep, complemento, cidade, uf
    // aparecem DENTRO da tabela "medicos" — sem JOIN necessário.
    //
    // Equivalente a escrever cada campo de Endereco diretamente na entidade Medico,
    // mas com código reutilizável — Paciente também usa @Embedded Endereco
    private Endereco endereco;
}

Como fica a tabela medicos no banco

Tabela: medicos (resultado de @Embedded Endereco) ┌────┬──────────────────┬───────────────────────┬───────────────┬──────────────┐ │ id │ nome │ email │ especialidade │ crm │ ├────┼──────────────────┼───────────────────────┼───────────────┼──────────────┤ │ 1 │ Dr. Joao Silva │ joao@clinic.com │ CARDIOLOGIA │ 123456 │ ├────┼──────────┬───────┼──────────────┬────────┼───────────────┴──────────────┤ │ │logradouro│bairro │ cep │ cidade │ uf │ complemento │ ativo │ ├────┼──────────┼───────┼──────────────┼────────┼────┼─────────────┼────────────┤ │ 1 │Rua Flores│Centro │ 01001000 │ SP │ SP │ Sala 202 │ true │ └────┴──────────┴───────┴──────────────┴────────┴────┴─────────────┴────────────┘ Todos na mesma tabela "medicos" — sem tabela "enderecos" separada!

@Embeddable vs @OneToOne — comparacao

@Embeddable + @Embedded (projeto VollMed)

  • Uma tabela: medicos (com colunas de endereço)
  • Sem JOIN: consulta mais simples e rapida
  • Endereco so existe vinculado a um pai (Medico ou Paciente)
  • Ideal quando endereco e parte do mesmo conceito
  • Reutilizavel: Paciente tambem usa o mesmo @Embeddable
  • Menos flexivel: nao tem id proprio, nao pode ser consultado sozinho

@OneToOne com tabela separada (alternativa)

  • Duas tabelas: medicos + enderecos
  • FK: medicos.endereco_id → enderecos.id
  • Precisa de JOIN nas consultas
  • Endereco tem id proprio — pode ser consultado sozinho
  • Mais flexivel: um endereco pode ter historico de mudancas
  • Mais complexo: mais tabelas, mais codigo, mais JOIN
Quando usar @Embeddable? Use @Embeddable quando o objeto embutido nao faz sentido existir de forma independente. Um endereco de medico so existe porque o medico existe. Um endeco de paciente so existe porque o paciente existe. Eles nao tem vida propria. Neste caso, @Embeddable e a escolha correta e mais simples. Se o endereco pudesse ser compartilhado entre multiplas entidades ou tivesse historico de versoes, ai valeria @OneToOne.
13

Exclusao Logica vs Exclusao Fisica

Existem duas formas de "excluir" um registro em sistemas de banco de dados. O SistemaVollMed implementa ambas — a fisica esta ativa e a logica esta comentada como alternativa recomendada para producao.

Exclusao Fisica — deleteById()

// ── EXCLUSÃO FÍSICA — remove o registro para sempre ─────────────────
@DeleteMapping("/{id}")
// URL: DELETE http://localhost:8080/medicos/5
// O {id} na URL é capturado pelo @PathVariable

@Transactional
// Necessário para garantir que o DELETE seja commitado

public void excluir(@PathVariable Integer id) {
    // @PathVariable Integer id: extrai o valor {id} da URL.
    // DELETE /medicos/5 → id = 5

    repository.deleteById(id);
    // Gera: DELETE FROM medicos WHERE id = 5
    //
    // ⚠️ Problema: se existirem consultas com este médico,
    // o DELETE viola a FK e lança exceção.
    // O dado some para sempre — não pode ser recuperado.
    // Em sistemas médicos, isso é problemático: perde histórico de consultas.
}

Exclusao Logica — ativo = false

// ── EXCLUSÃO LÓGICA — oculta o registro sem apagá-lo ────────────────
// (versão comentada no projeto — seria a alternativa recomendada)

// @DeleteMapping("/{id}")
// @Transactional
// public void alterarStatus(@PathVariable Integer id) {
//
//     var medico = repository.getReferenceById(id);
//     // Busca o médico — getReferenceById() não faz SELECT imediato (lazy proxy)
//
//     medico.exclusaoLogica();
//     // Chama o método da entidade que muda ativo = false
//     // @Transactional detecta a mudança (dirty checking) e gera:
//     // UPDATE medicos SET ativo = false WHERE id = 5
//     // O médico CONTINUA no banco — só o campo ativo mudou
// }

// ── Método exclusaoLogica() na entidade Medico ───────────────────────
public void exclusaoLogica() {
    this.ativo = false;
    // Muda apenas o campo booleano. O registro permanece intacto no banco.
    // Histórico de consultas, dados do médico: tudo preservado.
    // Para reativar: ativo = true (implementar um endpoint de reativação)
}

Comparacao detalhada

CriterioExclusao Fisica (DELETE)Exclusao Logica (ativo=false)
SQL geradoDELETE FROM medicos WHERE id=?UPDATE medicos SET ativo=false WHERE id=?
Dado no bancoRemovido permanentementePermanece (apenas marcado)
RecuperacaoImpossivel (a menos que tenha backup)Simples: ativo = true
HistoricoPerdido completamentePreservado — consultas antigas continuam validas
Integridade referencialViola FK se tiver consultas vinculadasSem problema — o registro continua existindo
ComplexidadeSimples — uma linha de codigoPrecisa de campo "ativo" e filtro nas listagens
LGPD / complianceApaga dados pessoais (pode ser necessario)Dados continuam no banco (precisa de cuidado)
Uso tipicoDados temporarios, logs, sessoesUsuarios, medicos, clientes — qualquer entidade de negocio

O campo ativo na entidade Medico

// Na entidade Medico.java:
private Boolean ativo = true;
// Boolean (objeto) em vez de boolean (primitivo) para aceitar null no banco.
// Valor padrão true: novos médicos começam ativos automaticamente.
// Sem precisar passar ativo no JSON de cadastro — o DTO não tem esse campo.

// Para listar só médicos ativos na listagem paginada, você adicionaria:
// No MedicoRepository: Page<Medico> findByAtivoTrue(Pageable pageable);
// No Controller: return repository.findByAtivoTrue(paginacao).map(DadosListagemMedico::new);
// Spring gera: SELECT * FROM medicos WHERE ativo = true LIMIT ? OFFSET ?
Por que exclusao logica e preferida em sistemas medicos? Em uma clinica medica, um medico que foi "desligado" ja realizou centenas de consultas. Se voce apagar o medico fisicamente: (1) as consultas ficam com a FK apontando para um id que nao existe mais — erro de integridade, (2) perde-se o historico clinico dos pacientes, (3) pode haver implicacoes legais. A exclusao logica preserva tudo e ainda permite reativar o medico se necessario.
14

Entidade Paciente — CRUD Completo

A entidade Paciente segue exatamente o mesmo padrao aprendido com Medico: Entidade + DTOs (Cadastro, Listagem, Atualizacao) + Repository + Controller. Isso demonstra que o padrao e reutilizavel e consistente para qualquer recurso da API.

Paciente.java

@Getter @Setter @AllArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(of = "id")
@Entity
@Table(name = "pacientes")
// Mesmas anotações do Médico: JPA + Lombok. Tabela se chama "pacientes"
public class Paciente {

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

    private String nome;
    private String email;
    private String telefone;

    private String cpf;
    // Diferença em relação ao Médico: CPF em vez de CRM.
    // Paciente se identifica pelo CPF (cidadão), não por registro profissional.

    // Nota: Paciente NÃO tem campo "ativo" nem enum "Especialidade"
    // Poderia ter em produção — mas neste projeto é mais simples

    @Embedded
    private Endereco endereco;
    // Mesmo @Embeddable do Médico — reutilizável em qualquer entidade

    public Paciente(DadosCadastroPaciente dados) {
        this.nome = dados.nome();
        this.email = dados.email();
        this.telefone = dados.telefone();
        this.cpf = dados.cpf();
        this.endereco = new Endereco(dados.endereco());
        // Mesmo padrão: converte DTO em Entidade no construtor
    }

    public void atualizarInformacoes(DadosAtualizacaoPaciente dados) {
        if (dados.nome() != null)     this.nome = dados.nome();
        if (dados.telefone() != null) this.telefone = dados.telefone();
        if (dados.endereco() != null)  this.endereco.atualizarInformacoes(dados.endereco());
        // Diferença: paciente não pode atualizar email (email é documento de conta).
        // DadosAtualizacaoPaciente não tem campo email por decisão de design.
    }
}

DTOs do Paciente

// ── DadosCadastroPaciente ────────────────────────────────────────────
public record DadosCadastroPaciente(
    String nome,
    String email,
    String telefone,
    String cpf,              // CPF ao invés de CRM
    DadosCadastroEndereco endereco
    // Neste projeto sem validações @NotBlank — pode adicionar em produção
) { }

// ── DadosListagemPaciente ────────────────────────────────────────────
public record DadosListagemPaciente(
    String nome,
    String email,
    String cpf               // só 3 campos — lista compacta para o frontend
) {
    public DadosListagemPaciente(Paciente paciente) {
        this(paciente.getNome(), paciente.getEmail(), paciente.getCpf());
        // Sem id, telefone, endereço — listagem simplificada
    }
}

// ── DadosAtualizacaoPaciente ─────────────────────────────────────────
public record DadosAtualizacaoPaciente(
    Integer id,
    String nome,
    String telefone,
    // ⚠️ email NÃO está aqui: paciente não pode alterar o próprio email
    //    (email geralmente é o login/identificador da conta)
    DadosCadastroEndereco endereco
) { }

PacienteRepository

package com.github.app.model.paciente;

import org.springframework.data.jpa.repository.JpaRepository;

// Sem @Repository explícito — o Spring detecta automaticamente
public interface PacienteRepository extends JpaRepository<Paciente, Integer> {
    // Mesmo padrão: JpaRepository<Paciente, Integer>
    // Ganha todos os métodos: save(), findAll(), findById(), deleteById()...
    // Corpo vazio: tudo que precisamos já está em JpaRepository
}

PacienteController — CRUD completo

@RestController
@RequestMapping("/pacientes")
// Note a barra inicial "/pacientes" — funciona igual a "pacientes" sem barra
public class PacienteController {

    @Autowired
    private PacienteRepository repository;

    // ── POST /pacientes ──────────────────────────────────────────────
    @PostMapping
    @Transactional
    public void cadastrar(@RequestBody DadosCadastroPaciente dados) {
        repository.save(new Paciente(dados));
        // Mesmo padrão do Médico: converte DTO em Entidade e salva
    }

    // ── GET /pacientes/todos ─────────────────────────────────────────
    @GetMapping("todos")
    public List<Paciente> listarTodos() {
        return repository.findAll();
        // Retorna entidade bruta — útil para debug, ruim para produção
    }

    // ── GET /pacientes/listar ────────────────────────────────────────
    @GetMapping("listar")
    public List<DadosListagemPaciente> listar() {
        return repository.findAll().stream()
                          .map(DadosListagemPaciente::new).toList();
    }

    // ── GET /pacientes?page=0&size=10 ────────────────────────────────
    @GetMapping
    public Page<DadosListagemPaciente> listarPorPagina(Pageable paginacao) {
        return repository.findAll(paginacao).map(DadosListagemPaciente::new);
    }

    // ── PUT /pacientes ───────────────────────────────────────────────
    @PutMapping
    @Transactional
    public void atualizar(@RequestBody DadosAtualizacaoPaciente dados) {
        var paciente = repository.getReferenceById(dados.id());
        paciente.atualizarInformacoes(dados);
        // Dirty checking: sem save() explícito — mesmo padrão do Médico
    }

    // ── DELETE /pacientes/{id} ───────────────────────────────────────
    @DeleteMapping("/{id}")
    @Transactional
    public void excluir(@PathVariable Integer id) {
        repository.deleteById(id);
    }
}

JSON para operacoes com Paciente

// ── POST /pacientes — Cadastrar ─────────────────────────────────────
{
  "nome": "Maria Oliveira",
  "email": "maria@email.com",
  "telefone": "11988887777",
  "cpf": "12345678901",
  "endereco": {
    "logradouro": "Rua das Acácias", "bairro": "Vila Nova",
    "cep": "04500000", "cidade": "Sao Paulo", "uf": "SP"
  }
}

// ── PUT /pacientes — Atualizar (só telefone) ─────────────────────────
{ "id": 1, "telefone": "11999990000" }

// ── GET /pacientes/listar — Resposta ─────────────────────────────────
[
  { "nome": "Maria Oliveira", "email": "maria@email.com", "cpf": "12345678901" }
]

Diferencas entre Medico e Paciente — tabela comparativa

AspectoMedicoPaciente
Identificador profissionalCRMCPF
EspecializacaoEspecialidade (enum)Nao tem
Campo ativoBoolean ativo = trueNao tem
Exclusao logicaexclusaoLogica() comentadaNao implementada
Email atualizavelSim (DadosAtualizacaoMedico tem email)Nao (DadosAtualizacaoPaciente nao tem email)
Tabela no bancomedicospacientes
RepositoryMedicoRepositoryPacienteRepository
15

Entidade Consulta — @ManyToOne

A Consulta e a entidade mais interessante do projeto porque ela relaciona um Medico com um Paciente. No banco de dados, isso se traduz em chaves estrangeiras (FK). No JPA, usamos @ManyToOne para mapear esse relacionamento.

Consulta.java — linha por linha

package com.github.app.model.consulta;

@Entity
@Getter @Setter @AllArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(of = "id")
@Table(name = "consultas")
// Mesmo padrão de Medico e Paciente: JPA + Lombok
public class Consulta {

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

    @ManyToOne
    // Relacionamento: MUITAS consultas podem ter o MESMO médico.
    // (Um médico atende muitas consultas. Cada consulta tem um médico.)
    // No banco: cria uma coluna FK na tabela "consultas" apontando para "medicos"

    @JoinColumn(name = "medicoId")
    // Define o nome da coluna de chave estrangeira: "medicoId".
    // Sem @JoinColumn: o Hibernate geraria um nome automático como "medico_id".

    private Medico medico;
    // Objeto Medico completo — JPA carrega automaticamente ao consultar uma Consulta.
    // A serialização para JSON inclui os dados do médico no objeto de resposta.

    @ManyToOne
    // Mesmo padrão: MUITAS consultas → UM paciente

    @JoinColumn(name = "pacienteId")
    // Coluna FK: "pacienteId" → referencia "pacientes.id"

    private Paciente paciente;

    @Enumerated(EnumType.STRING)
    private Status status;
    // Salva o texto do enum: "AGENDADA", "CONFIRMADA", "CANCELADA", "REALIZADA"

    private String observacao;
    // Texto livre: "Retorno pós-cirurgia", "Primeiro atendimento" etc.

    private LocalDateTime data;
    // Data e hora da consulta: 2026-06-15T10:30:00
    // LocalDateTime: sem fuso horário — adequado para uso local

    // ── Construtor de conversão: DTO → Entidade ──────────────────────
    public Consulta(DadosAgendamentoConsulta dados) {
        this.status = dados.status();
        this.observacao = dados.observacao();
        this.data = dados.data();
        // ⚠️ Note: medico e paciente NÃO são populados aqui!
        // O DTO tem apenas medicoId e pacienteId (inteiros).
        // O Controller busca os objetos completos e usa setMedico() e setPaciente().
        // Ver ConsultaController na Seção 16.
    }
}

Status.java — o enum de estados da consulta

public enum Status {
    AGENDADA,    // consulta marcada mas aguardando confirmação
    CONFIRMADA,  // paciente confirmou presença
    CANCELADA,   // consulta desmarcada (por qualquer motivo)
    REALIZADA;   // consulta já aconteceu — histórico
}
// Fluxo típico: AGENDADA → CONFIRMADA → REALIZADA
//               AGENDADA → CANCELADA  (paciente cancelou)

DadosAgendamentoConsulta — o DTO de entrada

public record DadosAgendamentoConsulta(

    Integer medicoId,
    // O cliente envia apenas o ID do médico, não o objeto completo.
    // Isso é o padrão REST: referenciar outros recursos pelo id.
    // O Controller busca o objeto Medico no banco a partir deste id.

    Integer pacienteId,
    // Mesmo raciocínio: só o id do paciente

    String observacao,
    // Texto livre — observação sobre a consulta

    Status status,
    // "AGENDADA" — valor do enum Status

    LocalDateTime data
    // "2026-06-15T10:30:00" — formato ISO 8601

) { }

Como ficam as tabelas no banco

Tabela: medicos ← referenciada pelas consultas ┌────┬──────────────────┬───────────────┐ │ id │ nome │ especialidade │ ├────┼──────────────────┼───────────────┤ │ 1 │ Dr. Joao Silva │ CARDIOLOGIA │ │ 3 │ Dra. Ana Costa │ GINECOLOGIA │ └────┴──────────────────┴───────────────┘ Tabela: pacientes ← referenciada pelas consultas ┌────┬──────────────────┬──────────────────┐ │ id │ nome │ cpf │ ├────┼──────────────────┼──────────────────┤ │ 2 │ Maria Oliveira │ 12345678901 │ │ 7 │ Carlos Santos │ 98765432100 │ └────┴──────────────────┴──────────────────┘ Tabela: consultas ← tem FKs para medicos e pacientes ┌────┬──────────┬────────────┬────────────┬────────────────────┬───────────────────────┐ │ id │ medicoId │ pacienteId │ status │ data │ observacao │ ├────┼──────────┼────────────┼────────────┼────────────────────┼───────────────────────┤ │ 1 │ 1 │ 2 │ AGENDADA │ 2026-06-10 14:00 │ Retorno de check-up │ │ 2 │ 3 │ 7 │ REALIZADA │ 2026-05-20 09:30 │ Consulta de rotina │ └────┴──────────┴────────────┴────────────┴────────────────────┴───────────────────────┘ ↑ ↑ FK → medicos.id FK → pacientes.id

O que e @ManyToOne — conceito e SQL

@ManyToOne: lendo o nome
  • Many (muitas) Consultas → One (um) Medico. Do ponto de vista da Consulta: ela aponta para UM medico.
  • No SQL: a tabela consultas tem uma coluna medicoId que e chave estrangeira para medicos.id.
  • O inverso (OneToMany) estaria na entidade Medico: "Um Medico tem Muitas Consultas" — mas neste projeto nao precisamos navegar nessa direcao.
  • @JoinColumn define o nome da coluna FK na tabela do lado Many (consultas).
RelacionamentoSignificadoAnotacao JPAOnde fica a FK
ManyToOneMuitas consultas → Um medico@ManyToOneNa tabela "muitos" (consultas)
OneToManyUm medico → Muitas consultas@OneToManyNa tabela "muitos" (consultas)
OneToOneUma entidade ↔ Uma entidade@OneToOneQualquer lado
ManyToManyMuitos ↔ Muitos@ManyToManyTabela de juncao intermediaria
16

ConsultaController — Agendando Consultas

O ConsultaController demonstra como usar multiplos repositories em um unico controller para buscar entidades relacionadas e compor um objeto mais complexo. Para agendar uma consulta, precisamos dos dados de medico, paciente e da propria consulta.

ConsultaController.java — linha por linha

package com.github.app.controller;

@RestController
@RequestMapping("consultas")
// Todos os endpoints deste controller começam com /consultas
public class ConsultaController {

    @Autowired
    private ConsultaRepository consultaRepository;
    // Repository da própria entidade Consulta: para salvar a consulta

    @Autowired
    private MedicoRepository medicoRepository;
    // Repository de Médico: para buscar o médico pelo id recebido no JSON
    // Por que precisamos? O DTO tem medicoId (Integer), mas a entidade
    // Consulta precisa de um objeto Medico completo (por causa do @ManyToOne)

    @Autowired
    private PacienteRepository pacienteRepository;
    // Mesma razão: DTO tem pacienteId, entidade precisa de objeto Paciente

    @PostMapping
    // Responde a: POST http://localhost:8080/consultas

    public Consulta agendar(@RequestBody DadosAgendamentoConsulta dados) {
        // Retorna Consulta (entidade) direto. Em produção, melhor retornar um DTO.
        // Aqui está ok para aprendizado — mas note que o JSON de resposta
        // incluirá o objeto Medico e Paciente completos aninhados.

        var medico = medicoRepository.getReferenceById(dados.medicoId());
        // Busca o médico pelo id: dados.medicoId() = o Integer do JSON.
        // getReferenceById() retorna um proxy lazy — não faz SELECT imediato.
        // Quando save() for chamado abaixo, o Hibernate resolve o proxy e
        // insere o medicoId correto na coluna FK da tabela consultas.

        var paciente = pacienteRepository.getReferenceById(dados.pacienteId());
        // Mesmo padrão: busca o paciente pelo id recebido no JSON

        var consulta = new Consulta(dados);
        // Cria o objeto Consulta a partir do DTO.
        // O construtor Consulta(dados) popula: status, observacao, data.
        // MAS NÃO popula medico e paciente (eles são objetos, não ids simples).

        consulta.setMedico(medico);
        // Setta o objeto Medico (proxy) na entidade Consulta.
        // Quando save() ocorrer, o Hibernate vai usar medico.getId() para
        // preencher a coluna medicoId na tabela consultas.

        consulta.setPaciente(paciente);
        // Mesmo raciocínio: setta o objeto Paciente

        return consultaRepository.save(consulta);
        // Gera: INSERT INTO consultas (medicoId, pacienteId, status, observacao, data)
        //       VALUES (1, 3, 'AGENDADA', 'Retorno...', '2026-06-15 10:30:00')
        // save() retorna a Consulta salva com o id gerado pelo banco.
        // Esse objeto com id é retornado como JSON na resposta.
    }
}

Por que usar setMedico() em vez de passar no construtor?

Construtor x Setter para referencias @ManyToOne O construtor Consulta(dados) so recebe o DTO DadosAgendamentoConsulta, que contem medicoId (Integer) — nao o objeto Medico. Voce poderia mudar o construtor para aceitar Medico e Paciente tambem, mas o padrao mais simples e usar o construtor para campos simples (status, data, observacao) e os setters do Lombok para as referencias de objetos. Os setters sao gerados automaticamente pelo @Setter do Lombok.

JSON para agendar uma consulta

// POST http://localhost:8080/consultas
// Content-Type: application/json
{
  "medicoId":   1,
  "pacienteId": 2,
  "status":     "AGENDADA",
  "observacao": "Retorno de check-up cardíaco",
  "data":       "2026-06-15T10:30:00"
}
// Requisito: o médico com id=1 e o paciente com id=2 devem existir no banco.
// Se não existirem: EntityNotFoundException ao acessar o proxy.

Resposta JSON — objeto Consulta salvo

// HTTP 200 OK — o objeto Consulta completo retornado
{
  "id": 1,
  "medico": {
    "id": 1,
    "nome": "Dr. Joao Silva",
    "email": "joao@clinic.com",
    "crm": "123456",
    "especialidade": "CARDIOLOGIA",
    "ativo": true,
    "endereco": { ... }
  },
  "paciente": {
    "id": 2,
    "nome": "Maria Oliveira",
    "email": "maria@email.com",
    "cpf": "12345678901",
    "endereco": { ... }
  },
  "status": "AGENDADA",
  "observacao": "Retorno de check-up cardíaco",
  "data": "2026-06-15T10:30:00"
}
// Note: retorna a entidade completa com médico e paciente aninhados.
// Em produção, crie um DadosDetalheConsulta (DTO) para controlar a resposta.

Passo a passo do que acontece no POST /consultas

1

JSON chega e e desserializado para DadosAgendamentoConsulta

O Jackson le {"medicoId": 1, "pacienteId": 2, "status": "AGENDADA"...} e cria o record.

2

getReferenceById() cria proxies lazy de Medico e Paciente

Neste momento nenhum SELECT foi feito ainda. O Hibernate apenas registra que "quando precisar do id 1 de Medico, busque no banco".

3

new Consulta(dados) cria o objeto parcialmente populado

Status, observacao, data sao preenchidos. Medico e Paciente ainda estao null.

4

setMedico() e setPaciente() completam o objeto

Os proxies (referencias ao banco) sao injetados na entidade Consulta.

5

consultaRepository.save() executa o INSERT

O Hibernate resolve os proxies, obtem os ids e gera: INSERT INTO consultas (medicoId, pacienteId, status, observacao, data) VALUES (1, 2, 'AGENDADA', ...)

6

Objeto salvo (com id gerado) e retornado como JSON

O banco gera id=1 automaticamente (AUTO_INCREMENT). O objeto Consulta agora tem id=1 e e serializado para JSON pelo Jackson.

Prerequisito: cadastre medico e paciente antes de agendar Para agendar uma consulta, o medico com medicoId e o paciente com pacienteId precisam existir no banco. Caso contrario, o getReferenceById() cria o proxy mas quando o Hibernate tenta resolver a FK para o INSERT, o id referenciado nao existe e lanca EntityNotFoundException. Sempre cadastre medico e paciente primeiro.
17

application.properties e Perfis

O Spring Boot usa arquivos .properties para configurar o comportamento da aplicacao. O VollMed usa perfis para separar a configuracao de diferentes ambientes (teste, desenvolvimento, producao) sem alterar codigo.

application.properties — arquivo principal

# ── application.properties ──────────────────────────────────────────
# Arquivo de configuração principal — sempre carregado.
# Define qual perfil específico também será carregado.

spring.profiles.active=test
# Define o perfil ativo: "test".
# Spring vai carregar TAMBÉM: application-test.properties.
# Os valores do perfil específico sobrescrevem os do principal.
# Para trocar o banco: mude para spring.profiles.active=dev

application-test.properties — banco H2 em memoria

# ── application-test.properties ─────────────────────────────────────
# Carregado quando spring.profiles.active=test
# Usa H2: banco em memória, sem instalação, ideal para desenvolvimento

spring.datasource.url=jdbc:h2:mem:clinicadb
# jdbc:h2      → driver H2
# :mem:        → banco em MEMÓRIA (não cria arquivo no disco)
# clinicadb    → nome do banco (pode ser qualquer nome)
# mem = memória: o banco existe SOMENTE enquanto a aplicação está rodando.
# Ao parar e reiniciar a aplicação: banco apagado, tabelas recriadas, dados perdidos.

spring.datasource.username=sa
# Usuário padrão do H2: "sa" (System Administrator)

spring.datasource.password=
# Senha vazia: padrão do H2 em modo memória

spring.h2.console.enabled=true
# Habilita a interface web do H2 Console.
# Acesse: http://localhost:8080/h2-console
# Na tela de login, use:
#   Driver Class: org.h2.Driver
#   JDBC URL:     jdbc:h2:mem:clinicadb  (EXATAMENTE igual ao configurado)
#   User Name:    sa
#   Password:     (em branco)
# Depois de conectar: você verá as tabelas criadas pelo Hibernate!

application-dev.properties — banco MySQL local

# ── application-dev.properties ──────────────────────────────────────
# Carregado quando spring.profiles.active=dev
# Usa MySQL: dados persistidos entre restarts da aplicação

spring.datasource.url=jdbc:mysql://localhost:3306/vollmed?createDatabaseIfNotExist=true
# jdbc:mysql   → driver MySQL
# localhost    → servidor na máquina local
# :3306        → porta padrão do MySQL
# vollmed      → nome do banco de dados
# createDatabaseIfNotExist → cria o banco se não existir

spring.datasource.username=root
spring.datasource.password=sua_senha_aqui
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Driver JDBC do MySQL (necessário adicionar dependência mysql-connector no pom.xml)

spring.jpa.hibernate.ddl-auto=update
# Define o que o Hibernate faz com o schema do banco ao iniciar.
# update: cria tabelas se não existirem, adiciona colunas novas.
#         Nunca apaga dados existentes — seguro para desenvolvimento.

spring.jpa.show-sql=true
# Imprime no console todos os SQLs que o Hibernate gera.
# Ótimo para depuração: ver qual SQL foi gerado para cada operação.

spring.jpa.properties.hibernate.format_sql=true
# Formata o SQL com indentação — mais fácil de ler no console

Como trocar de perfil

1

Editar application.properties

Mude spring.profiles.active=test para spring.profiles.active=dev. O Spring vai carregar application-dev.properties.

2

Via linha de comando (sem editar arquivo)

java -jar app.jar --spring.profiles.active=prod — sobrescreve o perfil configurado no arquivo sem alterá-lo.

3

Via variavel de ambiente

Definir a variavel de ambiente SPRING_PROFILES_ACTIVE=prod no sistema operacional ou container Docker.

Tabela de perfis disponiveis

PerfilArquivo carregadoBancoDados persistem?Uso tipico
testapplication-test.propertiesH2 em memoriaNao (apaga ao reiniciar)Desenvolvimento rapido, aulas
devapplication-dev.propertiesMySQL localSimDesenvolvimento com dados reais
prodapplication-prod.propertiesMySQL/PostgreSQL remotoSimAmbiente de producao

Valores do ddl-auto

ValorO que faz ao iniciar a appApaga dados?Quando usar
createApaga e recria todas as tabelasSIMTestes locais com dados limpos
create-dropCria ao iniciar, apaga ao pararSIMTestes automatizados
updateCria/adiciona colunas sem apagarNaoDesenvolvimento com MySQL
validateValida schema vs entidades, nao muda nadaNaoProducao com Flyway/Liquibase
noneNao faz nadaNaoProducao onde voce controla o schema
Como acessar o H2 Console Com o perfil test ativo e spring.h2.console.enabled=true, acesse http://localhost:8080/h2-console no navegador. Na tela de login, defina a JDBC URL como jdbc:h2:mem:clinicadb (exatamente como no properties), usuario sa, senha em branco. Voce vera as tabelas MEDICOS, PACIENTES, CONSULTAS criadas automaticamente pelo Hibernate.
Nunca use ddl-auto=create em producao O valor create apaga e recria todas as tabelas a cada restart da aplicacao. Em producao, isso significa perda total de dados. Use sempre validate ou none em producao, e gerencie o schema com ferramentas como Flyway ou Liquibase.
18

Erros Comuns e Como Resolver

Os erros mais frequentes ao trabalhar com Spring Boot REST pela primeira vez. Para cada erro: a mensagem tipica, a causa raiz e a solucao.

Erro 1 — HTTP 404

Endpoint nao encontrado — "No handler found"

Sintoma: O Insomnia retorna 404 Not Found ou {"status":404,"error":"Not Found"}.

Causas possiveis:

1. Classe sem @RestController — o Spring nao registra a classe como controller.

2. URL no Insomnia diferente da anotacao: @RequestMapping("medico") mas voce chama /medicos.

3. Metodo HTTP errado: endpoint e @PostMapping mas voce enviou GET.

4. Aplicacao nao foi reiniciada apos adicionar o Controller.

Solucao: Verifique se a classe tem @RestController. Confira o caminho exato em @RequestMapping e @GetMapping/@PostMapping. Use o console do Spring para ver a lista de endpoints registrados (inicia com "Mapped...").

Erro 2 — HTTP 400

Bad Request — validacao falhou

Sintoma: 400 Bad Request com mensagem sobre campos invalidos.

Causa: Um campo com @NotBlank, @NotNull ou @Email recebeu um valor invalido ou nao foi enviado no JSON.

Causa oculta frequente: O parametro no Controller nao tem @Valid: public void cadastrar(@RequestBody DadosCadastroMedico dados) — sem @Valid, a validacao nao e ativada e os dados chegam sem verificacao.

Solucao: Adicione @Valid antes do @RequestBody: @RequestBody @Valid DadosCadastroMedico dados. Verifique o JSON enviado — todos os campos @NotBlank devem estar presentes e nao-vazios.

Erro 3 — HTTP 500 Jackson

No serializer found for class / Infinite recursion

Sintoma: 500 Internal Server Error com mensagem No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor ou stack overflow.

Causa: Voce esta retornando a entidade JPA diretamente (ex: List<Medico> com campo @ManyToOne). O Jackson tenta serializar o proxy do Hibernate e falha. Em relacionamentos bidirecionais, entra em loop infinito.

Solucao: Sempre retorne DTOs (records) em vez da entidade diretamente. Troque List<Medico> por List<DadosListagemMedico>. Ou adicione @JsonIgnore nos campos que causam recursao (solucao menos elegante).

Erro 4 — EntityNotFoundException

Unable to find entity with id / entity not found

Sintoma: 500 Internal Server Error com jakarta.persistence.EntityNotFoundException: Unable to find com.github.app.model.medico.Medico with id 99.

Causa: Voce chamou getReferenceById(99) mas nao existe nenhum registro com id=99 no banco. O proxy foi criado, mas quando o Hibernate tentou resolver (ao acessar o objeto ou ao salvar um relacionamento), o id nao foi encontrado.

Solucao: Verifique que o id existe antes de chamar o endpoint. Use o H2 Console ou o endpoint de listagem para confirmar os ids disponiveis. Para tornar o erro mais amigavel em producao, trate a excecao com @ExceptionHandler.

Erro 5 — @Valid ignorado

Dados invalidos sao aceitos silenciosamente

Sintoma: Voce envia um JSON com email invalido ou nome vazio e a API aceita sem erro. O registro invalido e salvo no banco.

Causa: O @Valid esta faltando no parametro do Controller ou no campo do DTO aninhado. Sem ele, as anotacoes @NotBlank, @Email etc. sao completamente ignoradas em tempo de execucao.

Solucao: Adicione @Valid no Controller: public void cadastrar(@RequestBody @Valid DadosCadastroMedico dados). Para DTOs aninhados (DadosCadastroEndereco), tambem adicione @Valid no campo: @NotNull @Valid DadosCadastroEndereco endereco.

Erro 6 — @NoArgsConstructor ausente

No default constructor for entity / InstantiationException

Sintoma: A aplicacao inicia mas qualquer consulta ao banco lanca: org.hibernate.InstantiationException: No default constructor for entity: Medico.

Causa: A entidade nao tem o construtor sem argumentos. Isso acontece quando voce define um construtor customizado (como Medico(DadosCadastroMedico dados)) sem tambem definir (ou anotar com @NoArgsConstructor) o construtor padrao. O Java nao gera o construtor padrao automaticamente quando voce define um construtor.

Solucao: Adicione @NoArgsConstructor do Lombok na entidade. OU adicione manualmente public Medico() {}. Lembre-se: com @AllArgsConstructor + construtor customizado, voce PRECISA do @NoArgsConstructor explicitamente.

Erro 7 — H2 Console sem tabelas

H2 Console conecta mas nao exibe as tabelas do projeto

Sintoma: Voce acessa http://localhost:8080/h2-console, conecta, mas nao ve as tabelas MEDICOS, PACIENTES, CONSULTAS.

Causa 1: A JDBC URL no formulario do H2 Console esta diferente da configurada no application-test.properties. O H2 criou um banco diferente ao conectar.

Causa 2: O perfil test nao esta ativo — a aplicacao esta usando outro banco.

Solucao: Na tela de login do H2 Console, certifique-se que a JDBC URL e exatamente jdbc:h2:mem:clinicadb. Confirme que application.properties tem spring.profiles.active=test e que application-test.properties tem spring.h2.console.enabled=true.

Erro 8 — @Transactional ausente no PUT

UPDATE nao executado — dados nao sao atualizados no banco

Sintoma: Voce chama PUT /medicos, recebe 200 OK, mas ao listar os dados nao mudaram. Nenhum SQL de UPDATE aparece no console.

Causa: O metodo atualizar() nao tem @Transactional. Sem uma transacao ativa, o Hibernate nao gerencia o ciclo de vida da entidade carregada por getReferenceById(). O dirty checking nao funciona sem transacao — as mudancas no objeto Java sao descartadas ao final do metodo.

Solucao: Adicione @Transactional no metodo atualizar(). Alternativa: chame repository.save(medico) explicitamente no final (mas @Transactional + dirty checking e o padrao do Spring Data JPA).

Erro 9 — Porta 8080 em uso

Port 8080 was already in use

Sintoma: Ao iniciar a aplicacao: Web server failed to start. Port 8080 was already in use.

Causa: Outra instancia da aplicacao (ou outro servico) ja esta rodando na porta 8080. Frequente quando voce clica em Run sem parar a instancia anterior.

Solucao: Pare a instancia anterior no IntelliJ (botao Stop vermelho no console). Ou mude a porta em application.properties: server.port=8081.

Erro 10 — LazyInitializationException

LazyInitializationException: could not initialize proxy - no Session

Sintoma: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Medico.consultas - could not initialize proxy - no Session

Causa: Voce esta tentando acessar um relacionamento lazy (carregado sob demanda) fora de uma sessao JPA ativa — ou seja, apos o metodo do Controller ter encerrado a transacao.

Solucao: A forma mais simples e segura e usar DTOs — ao converter a entidade para DTO dentro do metodo @Transactional, todos os dados necessarios sao carregados antes da sessao fechar. Nunca retorne entidades com relacionamentos lazy diretamente no Controller.


Checklist de verificacao ao criar um novo recurso
  • Entidade com @Entity, @Id, @GeneratedValue, @NoArgsConstructor
  • DTOs com record para: Cadastro (entrada), Listagem (saida), Atualizacao (PUT)
  • Validacoes @NotBlank/@NotNull nos campos obrigatorios do DTO de Cadastro
  • @Valid no campo de DTO aninhado E no parametro do Controller
  • Repository extendendo JpaRepository com os tipos corretos
  • Controller com @RestController e @RequestMapping
  • @Transactional nos metodos POST, PUT e DELETE
  • @PathVariable para ids na URL e @RequestBody para JSON no corpo
  • Metodo atualizarInformacoes() com null-check em cada campo
  • Retornar DTOs nos GETs, nao entidades

Apostila — API REST com Spring Boot · SistemaVollMed · Will-firmino

Material didatico para ADS — Analise e Desenvolvimento de Sistemas.