Spring Data repositories — JpaRepository e query methods derivados

TL;DR

Basta declarar uma interface extends JpaRepository<T, ID> e o Spring Data gera a implementação em runtime via proxy. CRUD completo vem herdado (save, findById, findAll, delete, count, existsById). Para consultas extras, basta nomear o método seguindo as keywords da framework — o Spring lê o nome e gera o SQL correspondente, sem uma linha de implementação.


O que é

Um repositório Spring Data é uma interface Java que o desenvolvedor declara, mas nunca implementa. O Spring Data JPA escaneia as interfaces que estendem Repository (ou suas subinterfaces) e gera um proxy em runtime com toda a lógica de acesso ao banco.

O resultado prático: em vez de escrever um DAO com EntityManager, createQuery, abertura e fechamento de transação, basta declarar:

public interface OrderRepository extends JpaRepository<Order, Long> {
}

Esse repositório já tem dezenas de métodos funcionais — nenhuma implementação escrita à mão.


Por que importa

  • Elimina boilerplate de DAO: sem EntityManager explícito para operações básicas.
  • Consistência: todas as equipes usam o mesmo padrão de acesso a dados.
  • Entrevistas cobram dois pontos críticos:
    1. Como o Spring Data gera a query a partir do nome do método? (parsing do nome + geração de JPQL)
    2. Qual o limite das derived queries e quando usar @Query? (legibilidade e complexidade)

Como funciona

A hierarquia (RepositoryCrudRepositoryPagingAndSortingRepositoryJpaRepository)

Repository<T, ID>                  ← interface marcadora (sem métodos)
    └── CrudRepository<T, ID>      ← CRUD básico
            └── ListCrudRepository<T, ID>          ← igual ao Crud, mas retorna List
            └── PagingAndSortingRepository<T, ID>  ← findAll(Sort) e findAll(Pageable)
                    └── JpaRepository<T, ID>       ← JPA-específico: flush, saveAll, getReferenceById

JpaRepository herda tudo das interfaces acima e adiciona capacidades JPA: flush(), saveAllAndFlush(), deleteAllInBatch(), getReferenceById() (retorna proxy lazy sem hit no banco).

CRUD herdado (save / findById / findAll / delete / count / existsById)

MétodoComportamento
save(entity)persist se entidade nova, merge se existente (detectado pelo @Id)
findById(id)Retorna Optional<T>; SELECT por PK
findAll()SELECT * (cuidado com tabelas grandes)
delete(entity)Remove por referência
deleteById(id)Remove por PK
count()SELECT COUNT(*)
existsById(id)SELECT COUNT(*) > 0 — mais leve que findById quando só precisamos saber se existe

Derived queries: o SQL gerado do nome do método (keywords)

O Spring Data analisa o nome do método em duas partes:

  • Subject: o que fazer — find, count, exists, delete, remove
  • Predicate: os critérios — tudo depois de By

A partir dessas partes, o framework gera JPQL em runtime. Exemplos de keywords suportadas:

KeywordMétodo de exemploTrecho JPQL gerado
AndfindByStatusAndTotal(...)WHERE status = ? AND total = ?
OrfindByStatusOrCustomer(...)WHERE status = ? OR customer = ?
BetweenfindByTotalBetween(min, max)WHERE total BETWEEN ? AND ?
LessThanfindByTotalLessThan(val)WHERE total < ?
GreaterThanfindByTotalGreaterThan(val)WHERE total > ?
LikefindByDescriptionLike(pat)WHERE description LIKE ?
ContainingfindByDescriptionContaining(s)WHERE description LIKE %?%
InfindByStatusIn(collection)WHERE status IN (?, ?, ...)
True / FalsefindByActiveTrue()WHERE active = true
IgnoreCasefindByEmailIgnoreCase(e)LOWER(email) = LOWER(?)
OrderByfindByStatusOrderByCreatedAtDesc(s)ORDER BY created_at DESC
Top / FirstfindTop5ByStatus(s)LIMIT 5

O método pode combinar múltiplas keywords: findByStatusAndTotalGreaterThanOrderByCreatedAtDesc.

Quando a derived query fica ilegível → @Query (nota 09)

Derived queries são ótimas para condições simples. Quando o nome do método começa a parecer uma frase em inglês de três linhas, é hora de usar @Query com JPQL ou SQL nativo. Ver Consultas com @Query.


Na prática

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
 
// --- Entidade ---
@Entity
public class Order {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String externalId;
    private String status;
    private BigDecimal total;
 
    @ManyToOne
    private Customer customer;
 
    // getters e setters omitidos
}
 
// --- Repositório ---
public interface OrderRepository extends JpaRepository<Order, Long> {
 
    // Busca por campo simples
    List<Order> findByStatus(String status);
 
    // AND com comparação numérica
    List<Order> findByStatusAndTotalGreaterThan(String status, BigDecimal minTotal);
 
    // Busca por propriedade de associação (traversal)
    List<Order> findByCustomerEmail(String email);
 
    // COUNT derivado
    long countByStatus(String status);
 
    // EXISTS derivado — não carrega a entidade
    boolean existsByExternalId(String externalId);
 
    // Retorno Optional para consulta única
    Optional<Order> findByExternalId(String externalId);
 
    // Paginação
    org.springframework.data.domain.Page<Order> findByStatus(
            String status,
            org.springframework.data.domain.Pageable pageable);
}

O Spring Data registra OrderRepository como um bean gerenciado pelo container. Basta injetar normalmente:

@Service
public class OrderService {
 
    private final OrderRepository orderRepository;
 
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
 
    public List<Order> pendingHighValue(BigDecimal threshold) {
        return orderRepository.findByStatusAndTotalGreaterThan("PENDING", threshold);
    }
}

Armadilhas

(1) Derived query longa e ilegível

Métodos como findByCustomerCountryAndStatusInAndTotalGreaterThanOrderByCreatedAtDesc compila e funciona, mas é praticamente impossível de revisar em code review.

// Ruim — nome de 90 caracteres, difícil de auditar
List<Order> findByCustomerCountryAndStatusInAndTotalGreaterThanOrderByCreatedAtDesc(
    String country, List<String> statuses, BigDecimal min);
 
// Melhor — usar @Query com JPQL nomeado e legível
@Query("SELECT o FROM Order o WHERE o.customer.country = :country " +
       "AND o.status IN :statuses AND o.total > :min ORDER BY o.createdAt DESC")
List<Order> findHighValueByCountryAndStatuses(
    @Param("country") String country,
    @Param("statuses") List<String> statuses,
    @Param("min") BigDecimal min);

Fix: qualquer condição com mais de dois critérios ou com ordenação explícita é candidata a @Query.

(2) Usar findById quando bastava existsById

findById executa SELECT * e hidrata a entidade inteira na memória só para verificar existência.

// Ruim — busca toda a entidade para checar existência
boolean exists = orderRepository.findById(id).isPresent();
 
// Correto — SELECT COUNT(*) ou SELECT 1, sem hidratar objeto
boolean exists = orderRepository.existsById(id);

Fix: sempre preferir existsById (ou derived existsBy...) quando a entidade em si não é necessária.

(3) Assumir que toda derived query é eficiente

O Spring gera JPQL, mas o JPQL precisa ser traduzido para SQL pelo Hibernate e executado no banco. Traversals como findByCustomerAddressCityAndCustomerAddressCountry(...) podem gerar JOINs implícitos ou subqueries ineficientes dependendo do mapeamento.

// Parece simples, mas pode gerar JOIN não esperado
List<Order> findByCustomerAddressCity(String city);

Fix: ativar spring.jpa.show-sql=true (ou usar o log SQL do Hibernate) durante o desenvolvimento para inspecionar o SQL gerado. Para queries com JOINs complexos, usar @Query com JOIN FETCH explícito ou uma @EntityGraph.


Em entrevista

Frase pronta (inglês)

“Spring Data JPA generates the repository implementation at runtime as a proxy bean — you only declare the interface. CRUD methods are inherited from JpaRepository, which sits at the top of the hierarchy above CrudRepository and PagingAndSortingRepository. For custom queries, Spring Data parses the method name and derives JPQL from it using a fixed set of keywords like And, Or, Between, Containing, and OrderBy. When the method name would become unreadable — more than two predicates, complex joins, or native SQL — we switch to @Query with explicit JPQL or SQL to keep the intent clear and the query auditable.”

Vocabulário

Termo PTTermo EN
RepositórioRepository
Consulta derivadaDerived query
Método de consultaQuery method
PaginaçãoPagination
Palavra-chaveKeyword
ProxyProxy
Interface marcadoraMarker interface
OrdenaçãoSorting

Veja também


Referências