Spring Data JPA — Cheat Sheet
Hibernate & JPA · 5 topics. Download the PDF or the Instagram carousel and share it.
Spring Data JPA — JpaRepository
Extend JpaRepository<Entity, ID> to get findById, findAll, save, delete, and pagination out of the box; Spring Data generates the implementation at runtime.
- ✓JpaRepository provides CRUD, batch, and pagination APIs without any implementation code
- ✓Derived query method names are parsed into JPQL at startup — no SQL needed for simple queries
- ✓@Query accepts JPQL (entity/field names) or native SQL (nativeQuery=true)
- ✓@Modifying + @Transactional is required for UPDATE/DELETE @Query methods
- ✓Interface projections reduce fetched columns without writing DTO constructors
- ✓Pagination: return Page<T> and pass a Pageable parameter for count + data in one query
@Entity
public class User {
@Id @GeneratedValue Long id;
String email;
String status;
int age;
}
public interface UserRepository extends JpaRepository<User, Long> {
// SELECT u FROM User u WHERE u.email = ?1
Optional<User> findByEmail(String email);
// SELECT u FROM User u WHERE u.status = ?1 AND u.age >= ?2
List<User> findByStatusAndAgeGreaterThanEqual(String status, int minAge);
// SELECT COUNT(u) FROM User u WHERE u.status = ?1
long countByStatus(String status);
// SELECT u FROM User u WHERE u.email LIKE %?1%
List<User> findByEmailContaining(String fragment);
// Pagination: returns a Page<User> with total count
Page<User> findByStatus(String status, Pageable pageable);
}
// Usage
Page<User> page = userRepo.findByStatus("ACTIVE",
PageRequest.of(0, 20, Sort.by("email")));Query Methods by Method Name
Spring Data parses method names like findByEmailAndStatus, countByCategory, or deleteByExpiredBefore and generates the corresponding JPQL automatically.
- ✓Method name grammar: Subject (find/count/exists/delete) + By + Predicates (And/Or/Between...)
- ✓Property names are validated at startup — a typo in findByEmial causes a startup failure
- ✓Comparison keywords: GreaterThan, LessThan, Between, In, IsNull, Like, Containing, etc.
- ✓exists queries generate SQL EXISTS — more efficient than loading entities and checking size
- ✓Top/First limits rows at the query level: findTop5By…, findFirstBy…
- ✓For complex queries, switch to @Query — method names become unreadable beyond 2-3 predicates
public interface UserRepository extends JpaRepository<User, Long> {
// find by single field
Optional<User> findByEmail(String email);
// AND / OR
List<User> findByStatusAndRole(String status, String role);
List<User> findByEmailOrPhone(String email, String phone);
// Comparison keywords
List<User> findByAgeGreaterThan(int age);
List<User> findByCreatedAtBetween(LocalDateTime from, LocalDateTime to);
// String operations
List<User> findByNameContaining(String keyword); // LIKE %keyword%
List<User> findByEmailStartingWith(String prefix); // LIKE prefix%
// Null checks
List<User> findByPhoneIsNull();
List<User> findByPhoneIsNotNull();
// Ordered results
List<User> findByStatusOrderByCreatedAtDesc(String status);
}@Query Annotation
@Query overrides the derived query with explicit JPQL or native SQL; use @Modifying with @Transactional for bulk UPDATE or DELETE repository methods.
- ✓@Query accepts JPQL (default) or SQL (nativeQuery=true) inline on the repository method
- ✓Use @Param to bind named parameters; positional ?1 also works but is less readable
- ✓@Modifying is required for UPDATE/DELETE @Query methods — omitting it throws an exception
- ✓@Transactional must be present on the method or the calling service for @Modifying queries
- ✓clearAutomatically=true clears the L1 cache after bulk DML — prevents reading stale entities
- ✓JPQL constructor expression: SELECT new com.example.Dto(a, b) FROM Entity — maps to DTO constructor
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL — entity alias, relationship traversal
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customerId = :cid AND o.status = :status")
List<Order> findWithItemsByCustomer(@Param("cid") Long customerId,
@Param("status") String status);
// Projection with JPQL constructor expression
@Query("SELECT new com.example.dto.OrderSummary(o.id, o.status, o.totalAmount) " +
"FROM Order o WHERE o.createdAt >= :since")
List<OrderSummary> findSummariesSince(@Param("since") LocalDateTime since);
// Positional parameters (less readable but shorter)
@Query("SELECT o FROM Order o WHERE o.status = ?1 ORDER BY o.createdAt DESC")
List<Order> findByStatus(String status);
}Specifications & Predicates
JpaSpecificationExecutor lets you compose dynamic queries using Specification objects (reusable Criteria API predicates), ideal for complex filtered search endpoints.
- ✓Specification<T> wraps JPA Criteria API — returns a Predicate from Root, CriteriaQuery, CriteriaBuilder.
- ✓Extend JpaSpecificationExecutor<T> on the repository to get findAll(Specification, Pageable).
- ✓Compose with .and(), .or(), .not() — start with Specification.where(null) as the base.
- ✓Return cb.conjunction() (always true) when the filter argument is null — safe skip.
- ✓Add root.join() inside a Specification to filter on related entity fields.
- ✓Call query.distinct(true) when joining a collection to prevent duplicate result rows.
// Repository — extend JpaSpecificationExecutor
public interface OrderRepository extends
JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order> { // adds findAll(Specification, Pageable)
}
// Specification factory class
public class OrderSpecs {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) ->
status == null ? cb.conjunction() // no filter if null
: cb.equal(root.get("status"), status);
}
public static Specification<Order> forCustomer(Long customerId) {
return (root, query, cb) ->
customerId == null ? cb.conjunction()
: cb.equal(root.get("customerId"), customerId);
}
public static Specification<Order> createdAfter(LocalDate from) {
return (root, query, cb) ->
from == null ? cb.conjunction()
: cb.greaterThanOrEqualTo(root.get("createdAt"),
from.atStartOfDay());
}
public static Specification<Order> totalBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) return cb.conjunction();
if (min == null) return cb.lessThanOrEqualTo(root.get("total"), max);
if (max == null) return cb.greaterThanOrEqualTo(root.get("total"), min);
return cb.between(root.get("total"), min, max);
};
}
}Pagination & Sorting in Spring Data
Pass Pageable to repository methods; Spring Data returns Page<T> with content, total elements, and page metadata; Sort can be constructed dynamically from user input.
- ✓Page<T> runs two queries (data + COUNT); Slice<T> runs only the data query (use for infinite scroll)
- ✓PageRequest.of(page, size, sort) is 0-based — page 0 is the first page
- ✓Never pass raw user input to Sort.by() — validate against a whitelist first
- ✓JOIN FETCH or @EntityGraph with Pageable causes in-memory pagination — use keyset pagination instead
- ✓Slice.hasNext() checks whether a next page exists without knowing the total count
- ✓Sort.by("field").descending() and Sort.by(Direction, "field") are both valid
public interface OrderRepository extends JpaRepository<Order, Long> {
// Simple paginated query
Page<Order> findByStatus(String status, Pageable pageable);
// Paginated with @Query
@Query("SELECT o FROM Order o WHERE o.customerId = :cid ORDER BY o.createdAt DESC")
Page<Order> findByCustomer(@Param("cid") Long customerId, Pageable pageable);
}
// Service — page 0, size 20, sort by createdAt descending
Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<Order> page = orderRepository.findByStatus("PLACED", pageable);
log.info("Page {}/{}, {} total orders",
page.getNumber(), page.getTotalPages(), page.getTotalElements());
page.getContent().forEach(o -> process(o));