Pular para o conteúdo principal

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​

  1. Sintaxe JPQL (Java Persistence Query Language)
  2. @Query com JPQL
  3. @Query com SQL nativo
  4. Parâmetros nomeados com @Param
  5. 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​

AspectoJPQLSQL
AlvoEntidades JavaTabelas do BD
SintaxeFROM Product pFROM product
Camposp.nameproduct.name
Relaçõesp.category.nameJOIN 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:

  • @Modifying obrigatório
  • @Transactional obrigató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​

  1. Qual a diferença entre JPQL e SQL?
  2. Como passar parâmetros nomeados?
  3. O que faz JOIN FETCH?
  4. Quando usar @Modifying?

Respostas:

  1. JPQL usa entidades Java, SQL usa tabelas do banco de dados
  2. Com :nomeParam e @Param("nomeParam")
  3. Carrega as relações em uma única consulta (evita N+1)
  4. Para UPDATE ou DELETE

Próximo Passo​

Você domina @Query! Avance para Paginação e Performance.

➡️ Capítulo 4: Paginação e Performance

⬅️ Capítulo anterior: Relações

⬅️ Voltar ao Módulo 6