Capítulo 3: Consultas Personalizadas com @Query
Introdução
As convenções de nomenclatura são poderosas, mas às vezes insuficientes para consultas complexas.
Este capítulo ensina você a escrever suas próprias consultas com @Query!
O que você vai aprender
- Sintaxe JPQL (Java Persistence Query Language)
- @Query com JPQL
- @Query com SQL nativo
- Parâmetros nomeados com @Param
- Consultas de modificação (@Modifying)
Duração: 1h
Por que @Query?
Limites das Convenções
Tente criar esta consulta com convenções:
SELECT p FROM Product p
WHERE p.price < :maxPrice
AND p.stock > 0
AND p.category.name IN ('Electronics', 'Computers')
ORDER BY p.price ASC
Nome de método resultante:
findByPriceLessThanAndStockGreaterThanAndCategoriesNameInOrderByPriceAsc(
Double maxPrice,
Integer minStock,
List<String> categories
)
❌ Muito longo! Pouco legível!
Solução: @Query
@Query("SELECT p FROM Product p " +
"WHERE p.price < :maxPrice " +
"AND p.stock > 0 " +
"AND p.category.name IN :categories " +
"ORDER BY p.price ASC")
List<Product> findAvailableProducts(
@Param("maxPrice") Double maxPrice,
@Param("categories") List<String> categories
);
✅ Claro! Legível! Poderoso!
JPQL vs SQL
Diferenças Fundamentais
| Aspecto | JPQL | SQL |
|---|---|---|
| Alvo | Entidades Java | Tabelas do BD |
| Sintaxe | FROM Product p | FROM product |
| Campos | p.name | product.name |
| Relações | p.category.name | JOIN necessário |
| Portável | ✅ Sim | ❌ Depende do BD |
Exemplo Comparativo
JPQL (recomendado):
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findCheap(@Param("max") Double max);
SQL Nativo:
@Query(value = "SELECT * FROM product WHERE price < :max",
nativeQuery = true)
List<Product> findCheap(@Param("max") Double max);
Prefira JPQL: Mais portável, mais orientado a objetos!
JPQL: Conceitos Básicos
1. SELECT Simples
@Query("SELECT p FROM Product p")
List<Product> findAllProducts();
// Equivalente SQL:
// SELECT * FROM product
Pontos-chave:
Product= Nome da classe (não da tabela)p= Alias para a entidade
2. WHERE Condições
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
List<Product> findByPriceLessThan(@Param("maxPrice") Double maxPrice);
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);
@Query("SELECT p FROM Product p WHERE p.active = true AND p.stock > 0")
List<Product> findAvailable();
3. ORDER BY
@Query("SELECT p FROM Product p ORDER BY p.price ASC")
List<Product> findAllOrderByPrice();
@Query("SELECT p FROM Product p ORDER BY p.price DESC, p.name ASC")
List<Product> findAllSorted();
4. Joins
Sem JOIN explícito (usa a relação):
@Query("SELECT e FROM Employee e WHERE e.department.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);
Com JOIN:
@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);
Com JOIN FETCH (evita N+1):
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();
Parâmetros: Posicionais vs Nomeados
Parâmetros Posicionais (?1, ?2)
@Query("SELECT p FROM Product p WHERE p.price BETWEEN ?1 AND ?2")
List<Product> findByPriceRange(Double min, Double max);
Problema: Ordem importante, pouco claro
Parâmetros Nomeados (:name) - Recomendado
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
List<Product> findByPriceRange(
@Param("min") Double minPrice,
@Param("max") Double maxPrice
);
✅ Vantagens:
- Mais claro
- Ordem independente
- Menos erros
Consultas Complexas
Exemplo 1: Busca Multi-Critério
@Query("SELECT p FROM Product p " +
"WHERE (:name IS NULL OR p.name LIKE %:name%) " +
"AND (:minPrice IS NULL OR p.price >= :minPrice) " +
"AND (:maxPrice IS NULL OR p.price <= :maxPrice) " +
"AND (:category IS NULL OR p.category = :category)")
List<Product> searchProducts(
@Param("name") String name,
@Param("minPrice") Double minPrice,
@Param("maxPrice") Double maxPrice,
@Param("category") String category
);
Uso:
// Todos os parâmetros
productRepo.searchProducts("Laptop", 500.0, 1500.0, "Electronics");
// Somente o nome
productRepo.searchProducts("Laptop", null, null, null);
// Preço e categoria
productRepo.searchProducts(null, 500.0, 1500.0, "Electronics");
Exemplo 2: Agregações
// Contar
@Query("SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Long countByCategory(@Param("category") String category);
// Soma
@Query("SELECT SUM(p.price) FROM Product p WHERE p.category = :category")
Double sumPricesByCategory(@Param("category") String category);
// Média
@Query("SELECT AVG(p.price) FROM Product p")
Double getAveragePrice();
// Min/Max
@Query("SELECT MIN(p.price), MAX(p.price) FROM Product p")
Object[] getPriceRange();
Exemplo 3: GROUP BY e HAVING
@Query("SELECT p.category, COUNT(p) FROM Product p " +
"GROUP BY p.category " +
"HAVING COUNT(p) > :minCount")
List<Object[]> getCategoriesWithMinProducts(@Param("minCount") Long minCount);
Resultado:
List<Object[]> results = productRepo.getCategoriesWithMinProducts(5L);
for (Object[] row : results) {
String category = (String) row[0];
Long count = (Long) row[1];
System.out.println(category + ": " + count + " produtos");
}
Projeções: Retornando DTOs
Problema
Você não quer toda a entidade, apenas alguns campos:
// ❌ Carrega TODA a entidade
@Query("SELECT p FROM Product p")
List<Product> findAll();
Solução 1: Object[]
@Query("SELECT p.name, p.price FROM Product p")
List<Object[]> findNameAndPrice();
Uso:
List<Object[]> results = productRepo.findNameAndPrice();
for (Object[] row : results) {
String name = (String) row[0];
Double price = (Double) row[1];
}
❌ Não é type-safe!
Solução 2: Interface Projection
Crie uma interface:
public interface ProductSummary {
String getName();
Double getPrice();
}
Usar no @Query:
@Query("SELECT p.name AS name, p.price AS price FROM Product p")
List<ProductSummary> findSummaries();
Uso:
List<ProductSummary> summaries = productRepo.findSummaries();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}
✅ Type-safe!
Solução 3: DTO com Construtor
Crie um DTO:
public class ProductDTO {
private String name;
private Double price;
public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
// Getters
}
Usar no @Query:
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findDTOs();
✅ O melhor! Type-safe e performático!
SQL Nativo com nativeQuery = true
Quando Usar SQL Nativo?
Use SQL nativo para:
- ✅ Funções específicas do banco de dados (PostgreSQL, MySQL, etc.)
- ✅ Consultas muito complexas
- ✅ Otimizações específicas
Evite para:
- ❌ Consultas simples (use JPQL)
- ❌ Portabilidade entre bancos
Sintaxe SQL Nativo
@Query(value = "SELECT * FROM product WHERE price < :maxPrice",
nativeQuery = true)
List<Product> findCheapNative(@Param("maxPrice") Double maxPrice);
Pontos-chave:
nativeQuery = true- Nomes das tabelas e colunas do banco de dados (não das entidades Java)
Exemplo: Função PostgreSQL
@Query(value = "SELECT * FROM product " +
"WHERE LOWER(name) = LOWER(:name)",
nativeQuery = true)
List<Product> findByNameCaseInsensitive(@Param("name") String name);
Consultas de Modificação: @Modifying
UPDATE
@Modifying
@Transactional
@Query("UPDATE Product p SET p.price = :newPrice WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("newPrice") Double newPrice);
Pontos-chave:
@Modifyingobrigatório@Transactionalobrigatório- Retorna o número de linhas modificadas
DELETE
@Modifying
@Transactional
@Query("DELETE FROM Product p WHERE p.active = false")
int deleteInactiveProducts();
Atenção: Inconsistência de Cache
Problema:
Product p = productRepo.findById(1L); // Price = 100
productRepo.updatePrice(1L, 200.0); // Atualiza no BD
// Mas p.getPrice() ainda retorna 100! (cache)
Solução: Limpar o cache
@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Product p SET p.price = :price WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("price") Double price);
Named Queries
Definir na Entidade
@Entity
@NamedQuery(
name = "Product.findExpensive",
query = "SELECT p FROM Product p WHERE p.price > :minPrice"
)
public class Product {
// ...
}
Usar no Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// O Spring encontra automaticamente a NamedQuery!
List<Product> findExpensive(@Param("minPrice") Double minPrice);
}
Exemplos Completos
Repository de Produtos Completo
public interface ProductRepository extends JpaRepository<Product, Long> {
// 1. Busca simples
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);
// 2. Busca com join
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.price < :max")
List<Product> findCheapWithCategory(@Param("max") Double max);
// 3. Agregação
@Query("SELECT p.category.name, COUNT(p), AVG(p.price) " +
"FROM Product p GROUP BY p.category.name")
List<Object[]> getStatsByCategory();
// 4. Projeção DTO
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.active = true")
List<ProductDTO> findActiveDTOs();
// 5. SQL nativo
@Query(value = "SELECT * FROM product WHERE stock < 10",
nativeQuery = true)
List<Product> findLowStock();
// 6. Modificação
@Modifying
@Transactional
@Query("UPDATE Product p SET p.active = false WHERE p.stock = 0")
int deactivateOutOfStock();
}
Boas Práticas
1. Nomes de Métodos Claros
// ✅ BOM
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findByPriceLessThan(@Param("max") Double max);
// ❌ RUIM
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> get(@Param("max") Double max);
2. Parâmetros Nomeados
// ✅ BOM
@Query("... WHERE p.price BETWEEN :min AND :max")
List<Product> find(@Param("min") Double min, @Param("max") Double max);
// ❌ RUIM
@Query("... WHERE p.price BETWEEN ?1 AND ?2")
List<Product> find(Double min, Double max);
3. JOIN FETCH para Performance
// ❌ Problema N+1
@Query("SELECT e FROM Employee e")
List<Employee> findAll();
// Depois e.getDepartment() faz uma consulta por funcionário!
// ✅ 1 única consulta
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();
4. JPQL vs SQL Nativo
// ✅ Prefira JPQL (portável)
@Query("SELECT p FROM Product p WHERE p.price < :max")
// ⚠️ SQL nativo somente se necessário
@Query(value = "SELECT * FROM product WHERE price < :max", nativeQuery = true)
Resumo do Capítulo
O que o @Query Permite
✅ Consultas complexas mais legíveis
✅ JPQL (orientado a objetos, portável)
✅ SQL nativo (funções específicas do BD)
✅ Projeções (DTOs, interfaces)
✅ Agregações (COUNT, SUM, AVG)
✅ Modificações (@Modifying)
Sintaxe JPQL
@Query("SELECT e FROM Entity e " +
"WHERE e.field = :param " +
"ORDER BY e.field ASC")
List<Entity> findMethod(@Param("param") Type param);
SQL Nativo
@Query(value = "SELECT * FROM table WHERE column = :param",
nativeQuery = true)
List<Entity> findMethod(@Param("param") Type param);
Autoavaliação
- Qual a diferença entre JPQL e SQL?
- Como passar parâmetros nomeados?
- O que faz
JOIN FETCH? - Quando usar
@Modifying?
Respostas:
- JPQL usa entidades Java, SQL usa tabelas do banco de dados
- Com
:nomeParame@Param("nomeParam") - Carrega as relações em uma única consulta (evita N+1)
- Para UPDATE ou DELETE
Próximo Passo
Você domina @Query! Avance para Paginação e Performance.
➡️ Capítulo 4: Paginação e Performance