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.

Redação CPFHub.io
Redação CPFHub.io
··8 min de leitura
Como Armazenar Respostas da API de CPF no Banco de Dados Usando 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.

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
}
ColunaTipoDescrição
idBIGINT (PK)Identificador auto-incremento
cpf_hashVARCHAR (UNIQUE)Hash SHA-256 do CPF
cpf_cifradoVARCHARCPF criptografado com AES
nomeVARCHARNome retornado pela API
generoVARCHARGênero retornado pela API
data_nascimentoDATEData de nascimento
statusVARCHARPENDENTE, SUCESSO, FALHA
origemVARCHARcheckout, cadastro, lote
expira_emTIMESTAMPQuando o registro expira

Converter para criptografia transparente

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

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.

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.

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.

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 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.


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 — 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.

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

Sobre a redação

Redação CPFHub.io

Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.

WhatsAppFale conosco via WhatsApp