# Como Armazenar Respostas da API de CPF no Banco de Dados Usando JPA/Hibernate

> Aprenda a armazenar respostas da API de CPF no banco de dados usando JPA/Hibernate, com entidades, repositórios, cache e criptografia.

**Publicado:** 25/09/2024
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/armazenar-respostas-api-cpf-banco-dados-jpa-hibernate

---


Para armazenar respostas da API de CPF com JPA/Hibernate, crie uma entidade que mapeia os campos retornados, use um `AttributeConverter` para criptografar o CPF em repouso e implemente um repositório com queries que verifiquem validade do cache. Essa abordagem reduz chamadas repetidas à API, mantém histórico de validações e protege dados sensíveis conforme a LGPD.

## Introdução

Armazenar respostas da API de CPF no banco de dados é uma prática essencial para reduzir chamadas repetidas à API, manter histórico de validações e possibilitar análises posteriores. O JPA com Hibernate, padrão no ecossistema Spring, oferece mapeamento objeto-relacional, cache de segundo nível e suporte a criptografia transparente.

## Entidade JPA para dados de CPF

A entidade mapeia os dados retornados pela API para o banco de dados relacional.

```java
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "consultas_cpf", indexes = {
 @Index(name = "idx_cpf_hash", columnList = "cpfHash", unique = true),
 @Index(name = "idx_status", columnList = "status"),
 @Index(name = "idx_expira_em", columnList = "expiraEm")
})
public class ConsultaCpf {

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

 @Column(nullable = false, unique = true)
 private String cpfHash;

 @Convert(converter = CpfCryptoConverter.class)
 @Column(nullable = false)
 private String cpfCifrado;

 private String nome;
 private String nomeUpper;
 private String genero;
 private LocalDate dataNascimento;
 private Integer diaNascimento;
 private Integer mesNascimento;
 private Integer anoNascimento;

 @Enumerated(EnumType.STRING)
 @Column(nullable = false)
 private StatusConsulta status = StatusConsulta.PENDENTE;

 @Column(length = 500)
 private String motivoFalha;

 @Column(nullable = false)
 private String origem;

 @Column(nullable = false)
 private LocalDateTime consultadoEm;

 private LocalDateTime expiraEm;

 @Column(nullable = false, updatable = false)
 private LocalDateTime criadoEm;

 private LocalDateTime atualizadoEm;

 @PrePersist
 protected void onCreate() {
 criadoEm = LocalDateTime.now();
 if (consultadoEm == null) {
 consultadoEm = LocalDateTime.now();
 }
 }

 @PreUpdate
 protected void onUpdate() {
 atualizadoEm = LocalDateTime.now();
 }

 // Getters e setters omitidos por brevidade
 public Long getId() { return id; }
 public String getCpfHash() { return cpfHash; }
 public void setCpfHash(String cpfHash) { this.cpfHash = cpfHash; }
 public String getCpfCifrado() { return cpfCifrado; }
 public void setCpfCifrado(String cpfCifrado) { this.cpfCifrado = cpfCifrado; }
 public String getNome() { return nome; }
 public void setNome(String nome) { this.nome = nome; }
 public StatusConsulta getStatus() { return status; }
 public void setStatus(StatusConsulta status) { this.status = status; }
 public String getOrigem() { return origem; }
 public void setOrigem(String origem) { this.origem = origem; }
 public LocalDateTime getConsultadoEm() { return consultadoEm; }
 public void setConsultadoEm(LocalDateTime consultadoEm) { this.consultadoEm = consultadoEm; }
 public LocalDateTime getExpiraEm() { return expiraEm; }
 public void setExpiraEm(LocalDateTime expiraEm) { this.expiraEm = expiraEm; }
 public void setNomeUpper(String nomeUpper) { this.nomeUpper = nomeUpper; }
 public void setGenero(String genero) { this.genero = genero; }
 public void setDataNascimento(LocalDate dataNascimento) { this.dataNascimento = dataNascimento; }
 public void setDiaNascimento(Integer dia) { this.diaNascimento = dia; }
 public void setMesNascimento(Integer mes) { this.mesNascimento = mes; }
 public void setAnoNascimento(Integer ano) { this.anoNascimento = ano; }
 public void setMotivoFalha(String motivo) { this.motivoFalha = motivo; }

 public boolean isExpirada() {
 return expiraEm != null
 && expiraEm.isBefore(LocalDateTime.now());
 }
}

public enum StatusConsulta {
 PENDENTE, SUCESSO, FALHA
}
```

| Coluna | Tipo | Descrição |
|---|---|---|
| id | BIGINT (PK) | Identificador auto-incremento |
| cpf_hash | VARCHAR (UNIQUE) | Hash SHA-256 do CPF |
| cpf_cifrado | VARCHAR | CPF criptografado com AES |
| nome | VARCHAR | Nome retornado pela API |
| genero | VARCHAR | Gênero retornado pela API |
| data_nascimento | DATE | Data de nascimento |
| status | VARCHAR | PENDENTE, SUCESSO, FALHA |
| origem | VARCHAR | checkout, cadastro, lote |
| expira_em | TIMESTAMP | Quando o registro expira |

---

## Converter para criptografia transparente

O JPA AttributeConverter criptografa o CPF antes de gravar e decifra ao ler.

```java
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Converter
public class CpfCryptoConverter
 implements AttributeConverter<String, String> {

 private static final String ALGORITHM = "AES";
 private static final String SECRET_KEY =
 System.getenv("CPF_ENCRYPTION_KEY");

 @Override
 public String convertToDatabaseColumn(String cpf) {
 if (cpf == null) return null;
 try {
 SecretKeySpec key = new SecretKeySpec(
 SECRET_KEY.getBytes(StandardCharsets.UTF_8),
 ALGORITHM
 );
 Cipher cipher = Cipher.getInstance(ALGORITHM);
 cipher.init(Cipher.ENCRYPT_MODE, key);
 byte[] encrypted = cipher.doFinal(
 cpf.getBytes(StandardCharsets.UTF_8)
 );
 return Base64.getEncoder().encodeToString(encrypted);
 } catch (Exception e) {
 throw new RuntimeException(
 "Erro ao criptografar CPF", e
 );
 }
 }

 @Override
 public String convertToEntityAttribute(String dbData) {
 if (dbData == null) return null;
 try {
 SecretKeySpec key = new SecretKeySpec(
 SECRET_KEY.getBytes(StandardCharsets.UTF_8),
 ALGORITHM
 );
 Cipher cipher = Cipher.getInstance(ALGORITHM);
 cipher.init(Cipher.DECRYPT_MODE, key);
 byte[] decrypted = cipher.doFinal(
 Base64.getDecoder().decode(dbData)
 );
 return new String(decrypted, StandardCharsets.UTF_8);
 } catch (Exception e) {
 throw new RuntimeException(
 "Erro ao decifrar CPF", e
 );
 }
 }
}
```

---

## Repository com queries customizadas

O repository oferece métodos de busca otimizados para os casos de uso mais comuns.

```java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface ConsultaCpfRepository
 extends JpaRepository<ConsultaCpf, Long> {

 Optional<ConsultaCpf> findByCpfHash(String cpfHash);

 @Query("SELECT c FROM ConsultaCpf c " +
 "WHERE c.cpfHash = :hash " +
 "AND c.status = 'SUCESSO' " +
 "AND c.expiraEm > CURRENT_TIMESTAMP")
 Optional<ConsultaCpf> findValidByCpfHash(
 @Param("hash") String hash
 );

 List<ConsultaCpf> findByStatus(StatusConsulta status);

 @Query("SELECT c FROM ConsultaCpf c " +
 "WHERE c.consultadoEm < :data " +
 "AND c.status = 'SUCESSO' " +
 "ORDER BY c.consultadoEm ASC")
 List<ConsultaCpf> findAntigas(
 @Param("data") LocalDateTime data
 );

 @Modifying
 @Query("DELETE FROM ConsultaCpf c " +
 "WHERE c.expiraEm < CURRENT_TIMESTAMP")
 int deleteExpiradas();

 @Query("SELECT c.status, COUNT(c) FROM ConsultaCpf c " +
 "GROUP BY c.status")
 List<Object[]> countByStatus();
}
```

---

## Service de consulta e armazenamento

O service orquestra consulta à API, armazenamento e leitura do cache no banco.

```java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Service
public class CpfLookupService {

 private static final int TTL_HORAS = 24;
 private final ConsultaCpfRepository repository;
 private final CpfApiClient apiClient;

 public CpfLookupService(
 ConsultaCpfRepository repository,
 CpfApiClient apiClient) {
 this.repository = repository;
 this.apiClient = apiClient;
 }

 @Transactional
 public ConsultaCpf consultar(String cpf, String origem) {
 String cpfLimpo = cpf.replaceAll("\\D", "");
 String hash = gerarHash(cpfLimpo);

 // Verificar cache no banco
 Optional<ConsultaCpf> cached =
 repository.findValidByCpfHash(hash);
 if (cached.isPresent()) {
 return cached.get();
 }

 // Consultar API
 ResultadoCpf resultado = apiClient.consultar(cpfLimpo);

 // Persistir resultado
 ConsultaCpf consulta = new ConsultaCpf();
 consulta.setCpfHash(hash);
 consulta.setCpfCifrado(cpfLimpo);
 consulta.setOrigem(origem);
 consulta.setConsultadoEm(LocalDateTime.now());

 if (resultado.sucesso()) {
 consulta.setNome(resultado.nome());
 consulta.setGenero(resultado.genero());
 consulta.setDataNascimento(
 LocalDate.parse(resultado.dataNascimento())
 );
 consulta.setStatus(StatusConsulta.SUCESSO);
 consulta.setExpiraEm(
 LocalDateTime.now().plusHours(TTL_HORAS)
 );
 } else {
 consulta.setStatus(StatusConsulta.FALHA);
 consulta.setMotivoFalha(resultado.erro());
 consulta.setExpiraEm(
 LocalDateTime.now().plusHours(1)
 );
 }

 return repository.save(consulta);
 }

 private String gerarHash(String cpf) {
 try {
 MessageDigest digest =
 MessageDigest.getInstance("SHA-256");
 byte[] hash = digest.digest(cpf.getBytes());
 StringBuilder hexString = new StringBuilder();
 for (byte b : hash) {
 hexString.append(
 String.format("%02x", b)
 );
 }
 return hexString.toString();
 } catch (Exception e) {
 throw new RuntimeException(e);
 }
 }
}
```

---

## Manutenção e limpeza agendada

Use o Spring Scheduler para limpar registros expirados automaticamente.

```java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class CpfMaintenanceTask {

 private final ConsultaCpfRepository repository;

 public CpfMaintenanceTask(ConsultaCpfRepository repository) {
 this.repository = repository;
 }

 @Scheduled(cron = "0 0 3 * * *") // Todo dia às 3h
 @Transactional
 public void limparExpiradas() {
 int removidos = repository.deleteExpiradas();
 System.out.printf(
 "[Manutencao] %d consultas expiradas removidas%n",
 removidos
 );
 }

 @Scheduled(cron = "0 0 6 1 * *") // Dia 1 de cada mês
 public void gerarRelatorio() {
 List<Object[]> contagens = repository.countByStatus();
 System.out.println("--- Relatorio Mensal ---");
 for (Object[] row : contagens) {
 System.out.printf(" %s: %d%n", row[0], row[1]);
 }
 }
}
```

---

## Perguntas frequentes

### O que é necessário para implementar validação de CPF neste contexto?
A validação de CPF exige uma chamada à API com o número do documento e a chave de autenticação. A CPFHub.io retorna o status do CPF, nome do titular e data de nascimento em menos de 200ms, permitindo a verificação em tempo real durante o cadastro ou transação.

### A API CPFHub.io funciona para todos os volumes de consulta?
Sim. O plano gratuito oferece 50 consultas por mês sem cartão de crédito — ideal para testes e projetos pequenos. Para volumes maiores, o plano Pro inclui 1.000 consultas mensais por R$149. Se o limite for ultrapassado, a API não bloqueia: cobra R$0,15 por consulta adicional.

### Como garantir conformidade com a LGPD ao usar uma API de CPF?
Use o CPF apenas para a finalidade declarada ao titular, armazene apenas o necessário (não guarde o CPF cru se um token bastar), implemente controle de acesso aos logs de consulta e documente a base legal para o tratamento. A [ANPD](https://www.gov.br/anpd) orienta que dados de identificação devem ser tratados com o princípio da necessidade.

### Quanto tempo leva para integrar a API CPFHub.io?
A integração básica leva menos de 30 minutos: crie uma conta em cpfhub.io, gere a API key no painel e faça uma chamada GET para `https://api.cpfhub.io/cpf/{CPF}` com o header `x-api-key`. A documentação inclui exemplos em Python, Node.js, PHP, Java e outras linguagens.

### Leia também

- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [SLA de API de CPF: níveis de disponibilidade e o que exigir do seu provedor](https://cpfhub.io/blog/sla-api-cpf-niveis-disponibilidade)
- [API de CPF grátis para desenvolvedores: como começar em 5 minutos](https://cpfhub.io/blog/api-cpf-gratis-desenvolvedores-comecar-5-minutos)
- [10 erros mais comuns ao integrar uma API de CPF e como evitá-los](https://cpfhub.io/blog/10-erros-mais-comuns-ao-integrar-uma-api-de-cpf)

---

## Conclusão

Armazenar respostas da API de CPF com JPA/Hibernate em Spring Boot oferece uma solução robusta com criptografia transparente, cache no banco de dados e manutenção automatizada. O mapeamento objeto-relacional simplifica a persistência e as queries customizadas garantem performance nas buscas mais frequentes. A criptografia com AttributeConverter protege os dados sensíveis de CPF em repouso, mantendo conformidade com a LGPD.

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/) — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a persistir respostas de CPF com segurança e eficiência em minutos.

