Querying — Cheat Sheet
Hibernate & JPA · 5 topics. Download the PDF or the Instagram carousel and share it.
JPQL Basics
JPQL is an entity-oriented query language; queries reference entity and field names (not table/column names), making them portable across DB vendors.
- ✓JPQL uses entity class names and Java field names — never SQL table or column names.
- ✓Use named parameters (:paramName) in JPQL — @Param("name") maps the argument; positional parameters (?1) are fragile.
- ✓SELECT new com.example.MyDto(o.id, o.name) constructor expression maps results to a DTO without loading full entities.
- ✓@Modifying + @Transactional is required for @Query UPDATE and DELETE; omitting either causes an exception.
- ✓JPQL JOIN automatically follows entity relationships (mappedBy, @JoinColumn) — no ON clause needed.
- ✓For DB-specific features (window functions, CTEs, MATCH AGAINST) use nativeQuery=true — JPQL cannot express everything SQL can.
// SQL reference for comparison:
// SELECT id, total, status FROM orders o WHERE o.customer_id = 42 AND o.status = 'SHIPPED'
// Equivalent JPQL — uses class name "Order", field names "customerId", "status"
// Entity: @Entity public class Order { Long id; Long customerId; String status; BigDecimal total; }
// "o" is an alias (like SQL AS)
String jpql = "SELECT o FROM Order o WHERE o.customerId = :cid AND o.status = :status";
// Executing with EntityManager
TypedQuery<Order> query = em.createQuery(jpql, Order.class);
query.setParameter("cid", 42L);
query.setParameter("status", "SHIPPED");
List<Order> orders = query.getResultList();
// SELECT single field (returns List<BigDecimal>)
TypedQuery<BigDecimal> totalsQuery =
em.createQuery("SELECT o.total FROM Order o WHERE o.customerId = :cid", BigDecimal.class);
totalsQuery.setParameter("cid", 42L);
List<BigDecimal> totals = totalsQuery.getResultList();
// Aggregate functions
TypedQuery<Long> countQuery =
em.createQuery("SELECT COUNT(o) FROM Order o WHERE o.status = :s", Long.class);
countQuery.setParameter("s", "PENDING");
long count = countQuery.getSingleResult();JPQL Joins & Fetch Joins
Regular JPQL JOIN does not initialise the lazy collection; JOIN FETCH forces Hibernate to load it in the same query, preventing N+1 selects for that association.
- ✓JOIN in JPQL filters but does NOT initialise the lazy association (N+1 still occurs on access).
- ✓JOIN FETCH loads the association in the same SQL query — eliminates N+1 for that path.
- ✓JOIN FETCH on a collection + Pageable triggers in-memory pagination — use @EntityGraph instead.
- ✓Fetching two List collections in one query causes MultipleBagFetchException.
- ✓Fix: change List to Set, or load each collection in a separate query.
- ✓LEFT JOIN FETCH includes the parent even when the child collection is empty.
// Regular JOIN — use for filtering only, does NOT initialise the collection
@Query("SELECT o FROM Order o " +
"JOIN o.customer c " +
"WHERE c.tier = 'GOLD'")
List<Order> findGoldCustomerOrders();
// SQL: SELECT o.* FROM orders o JOIN customers c ON o.customer_id=c.id WHERE c.tier='GOLD'
// Accessing order.getCustomer() later triggers N lazy selects!
// JOIN FETCH — loads the association in the SAME query
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.customer " +
"WHERE o.status = :status")
List<Order> findWithCustomerByStatus(@Param("status") OrderStatus status);
// SQL: SELECT o.*, c.* FROM orders o JOIN customers c ON o.customer_id=c.id WHERE o.status=?
// Accessing order.getCustomer() hits the L1 cache — no additional SQL
// LEFT JOIN FETCH — include orders even if customer is null
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.customer WHERE o.id = :id")
Optional<Order> findWithOptionalCustomer(@Param("id") Long id);Named Queries
@NamedQuery precompiles JPQL at startup, enabling early syntax validation and potential query plan caching; defined on the entity class for discoverability.
- ✓@NamedQuery JPQL is validated at startup — syntax errors fail fast, not at runtime
- ✓SQL translation is cached per named query — no re-parsing cost per execution
- ✓Convention: name = "EntityName.descriptiveName" for Spring Data auto-detection
- ✓Spring Data looks for a matching @NamedQuery before generating a derived query
- ✓@NamedNativeQuery holds raw SQL; use @SqlResultSetMapping to map results to a DTO
- ✓With Spring Data @Query, explicit inline queries are often clearer than @NamedQuery
@Entity
@Table(name = "orders")
@NamedQueries({
@NamedQuery(
name = "Order.findByCustomer",
query = "SELECT o FROM Order o WHERE o.customerId = :customerId ORDER BY o.createdAt DESC"
),
@NamedQuery(
name = "Order.countPendingByCustomer",
query = "SELECT COUNT(o) FROM Order o WHERE o.customerId = :customerId AND o.status = 'PENDING'"
)
})
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long customerId;
private String status;
private LocalDateTime createdAt;
}Criteria API
The Criteria API builds type-safe dynamic queries programmatically using metamodel classes; more verbose than JPQL but catches column/type errors at compile time.
- ✓Criteria API provides compile-time type safety; JPQL strings fail at runtime when field names change.
- ✓The Hibernate JPA metamodel generator (hibernate-jpamodelgen) produces entity_class_ files with SingularAttribute/ListAttribute fields.
- ✓Build predicates conditionally and combine with cb.and() for dynamic optional-filter search endpoints.
- ✓Spring Data Specifications are the recommended abstraction — cleaner than raw Criteria, composable with .and()/.or().
- ✓CriteriaQuery<T> vs CriteriaQuery<Tuple>: use Tuple for multi-column projections that do not map to a single entity.
- ✓For very complex reports, native SQL is often simpler than deep Criteria nesting — choose the right tool for readability.
<!-- pom.xml — metamodel generator -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.4.0.Final</version>
<scope>provided</scope>
</dependency>
// Generated: Order_.java (do not edit — auto-generated)
// @StaticMetamodel(Order.class)
// public abstract class Order_ {
// public static volatile SingularAttribute<Order, Long> id;
// public static volatile SingularAttribute<Order, String> status;
// public static volatile SingularAttribute<Order, BigDecimal> amount;
// public static volatile ListAttribute<Order, OrderItem> items;
// }
// Basic Criteria query
public List<Order> findByStatus(String status) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
query.select(root)
.where(cb.equal(root.get(Order_.status), status))
.orderBy(cb.desc(root.get(Order_.amount)));
return em.createQuery(query)
.setMaxResults(100)
.getResultList();
}Native SQL Queries
@Query(nativeQuery=true) sends raw SQL to the DB; useful for DB-specific features or complex queries that JPQL cannot express; results can map to entities or projections.
- ✓nativeQuery=true sends raw SQL — bypasses JPQL and uses DB-specific features directly
- ✓Always use named (:param) or positional (?1) bind parameters — never concatenate strings
- ✓Result mapping: entity class, projection interface, or @SqlResultSetMapping
- ✓Pagination requires an explicit countQuery — Spring Data cannot derive it from complex SQL
- ✓Native queries bypass Hibernate's entity graph — no automatic lazy loading of associations
- ✓Use JPQL for portability; native SQL when JPQL cannot express the required query
public interface OrderRepository extends JpaRepository<Order, Long> {
// Maps result columns to Order entity fields
@Query(value = "SELECT * FROM orders WHERE customer_id = :customerId AND status = :status",
nativeQuery = true)
List<Order> findByCustomerAndStatus(@Param("customerId") Long customerId,
@Param("status") String status);
// Projection interface — selects subset of columns
@Query(value = "SELECT id, status, total_amount FROM orders WHERE customer_id = :customerId",
nativeQuery = true)
List<OrderSummary> findSummariesByCustomer(@Param("customerId") Long customerId);
}
// Projection interface
public interface OrderSummary {
Long getId();
String getStatus();
BigDecimal getTotalAmount();
}