Spring Boot
Spring Data JPA
Spring Data JPA eliminates boilerplate data access code by generating repository implementations at startup from interface definitions. Declare a method named findByEmailAndRole and Spring Data writes the query. Add @Query for custom JPQL or native SQL. Layer in Pageable for pagination and Sort for ordering — all without writing a single line of JDBC or Hibernate session management code.
Repository Hierarchy
Spring Data JPA provides a hierarchy of repository interfaces. Each level adds more functionality. Choose the lowest level that provides what you need — CrudRepository for basic CRUD, PagingAndSortingRepository when pagination is required, JpaRepository for the full feature set including bulk operations and flushing.
Java
// ── Repository hierarchy (each extends the one above): ───────────────
//
// Repository<T, ID> (marker interface — no methods)
// └── CrudRepository<T, ID> (save, findById, findAll, delete, count)
// └── PagingAndSortingRepository (findAll(Pageable), findAll(Sort))
// └── JpaRepository<T, ID> (flush, saveAllAndFlush, deleteAllInBatch)
// ── JpaRepository — the standard choice for Spring Boot applications: ─
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// JpaRepository<User, Long>:
// User = the entity type
// Long = the primary key type
// Inherited methods (no implementation needed):
// save(User) → INSERT or UPDATE
// saveAll(Iterable<User>) → batch INSERT or UPDATE
// findById(Long) → Optional<User>
// findAll() → List<User>
// findAll(Pageable) → Page<User>
// findAll(Sort) → List<User>
// findAllById(Iterable<Long>) → List<User>
// existsById(Long) → boolean
// count() → long
// deleteById(Long)
// delete(User)
// deleteAll()
// deleteAllById(Iterable<Long>)
// flush() → flush pending changes to DB
// saveAndFlush(User) → save + immediate flush
}
// ── CrudRepository — when pagination is not needed: ───────────────────
@Repository
public interface TagRepository extends CrudRepository<Tag, Long> { }
// ── ListCrudRepository (Spring Data 3+) — returns List instead of Iterable:
@Repository
public interface CategoryRepository extends ListCrudRepository<Category, Long> { }Derived Query Methods
Spring Data JPA generates query implementations from method names at startup. The method name is parsed into a JPQL query — no @Query annotation required. This covers the majority of simple query needs.
Java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ── Find by single field: ─────────────────────────────────────────
Optional<User> findByEmail(String email);
List<User> findByRole(User.Role role);
List<User> findByName(String name);
// ── Find by multiple fields (AND): ────────────────────────────────
Optional<User> findByEmailAndRole(String email, User.Role role);
List<User> findByNameAndActiveTrue(String name);
// ── Find by multiple fields (OR): ─────────────────────────────────
List<User> findByNameOrEmail(String name, String email);
// ── Comparison operators: ─────────────────────────────────────────
List<User> findByAgGreaterThan(int age);
List<User> findByAgeGreaterThanEqual(int age);
List<User> findByAgeLessThan(int age);
List<User> findByAgeBetween(int min, int max);
List<User> findByCreatedAtAfter(LocalDateTime date);
List<User> findByCreatedAtBefore(LocalDateTime date);
// ── String matching: ──────────────────────────────────────────────
List<User> findByNameContaining(String fragment); // LIKE %fragment%
List<User> findByNameStartingWith(String prefix); // LIKE prefix%
List<User> findByNameEndingWith(String suffix); // LIKE %suffix
List<User> findByNameContainingIgnoreCase(String fragment); // case-insensitive
// ── Null checks: ──────────────────────────────────────────────────
List<User> findByBioIsNull();
List<User> findByBioIsNotNull();
// ── Boolean flags: ────────────────────────────────────────────────
List<User> findByActiveTrue();
List<User> findByActiveFalse();
// ── In / Not In: ──────────────────────────────────────────────────
List<User> findByRoleIn(Collection<User.Role> roles);
List<User> findByIdNotIn(Collection<Long> ids);
// ── Sorting built into the method name: ───────────────────────────
List<User> findByRoleOrderByNameAsc(User.Role role);
List<User> findByActiveOrderByCreatedAtDesc(boolean active);
// ── Existence and count: ──────────────────────────────────────────
boolean existsByEmail(String email);
long countByRole(User.Role role);
long countByActiveTrue();
// ── Delete: ───────────────────────────────────────────────────────
void deleteByEmail(String email);
long deleteByActiveFalse(); // returns count of deleted rows
// ── Top / First — limit results: ──────────────────────────────────
Optional<User> findFirstByOrderByCreatedAtDesc(); // most recent
List<User> findTop5ByRoleOrderByCreatedAtDesc(User.Role role);
List<User> findFirst10ByActiveTrue();
// ── With Pageable — any finder can accept Pageable: ───────────────
Page<User> findByRole(User.Role role, Pageable pageable);
List<User> findByActiveTrue(Sort sort);
}@Query — Custom JPQL and Native SQL
When derived method names become unwieldy or the query requires features not expressible through naming conventions, @Query lets you write JPQL or native SQL directly. JPQL operates on entities and fields; native SQL operates on tables and columns.
Java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ── JPQL query — operates on entity fields, not column names: ─────
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmailJpql(@Param("email") String email);
// ── JPQL with JOIN FETCH — prevents N+1: ─────────────────────────
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// ── JPQL with multiple conditions: ───────────────────────────────
@Query("""
SELECT u FROM User u
WHERE (:role IS NULL OR u.role = :role)
AND (:active IS NULL OR u.active = :active)
AND (:search IS NULL OR LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%')))
""")
Page<User> search(
@Param("role") User.Role role,
@Param("active") Boolean active,
@Param("search") String search,
Pageable pageable);
// ── @Query with Pageable — must provide countQuery for Page<T>: ───
@Query(value = "SELECT u FROM User u WHERE u.role = :role",
countQuery = "SELECT COUNT(u) FROM User u WHERE u.role = :role")
Page<User> findByRolePaged(@Param("role") User.Role role, Pageable pageable);
// ── Native SQL — operates on actual table/column names: ───────────
@Query(value = "SELECT * FROM users WHERE email = :email",
nativeQuery = true)
Optional<User> findByEmailNative(@Param("email") String email);
// ── Native SQL with pagination: ───────────────────────────────────
@Query(value = "SELECT * FROM users WHERE role = :role ORDER BY created_at DESC",
countQuery = "SELECT COUNT(*) FROM users WHERE role = :role",
nativeQuery = true)
Page<User> findByRoleNative(@Param("role") String role, Pageable pageable);
// ── Modifying query — UPDATE or DELETE: ───────────────────────────
@Modifying
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :cutoff")
int deactivateInactiveUsers(@Param("cutoff") LocalDateTime cutoff);
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.active = false AND u.createdAt < :cutoff")
int deleteOldInactiveUsers(@Param("cutoff") LocalDateTime cutoff);
// ── Projections — select specific fields only: ────────────────────
@Query("SELECT u.id AS id, u.name AS name, u.email AS email FROM User u")
List<UserSummary> findAllSummaries();
}
// ── Projection interface — maps query results to fields: ───────────────
public interface UserSummary {
Long getId();
String getName();
String getEmail();
}Pagination and Sorting
Pageable and Sort are first-class parameters in Spring Data JPA. Pass a Pageable to any repository method to get a Page<T> back — containing the content, total element count, total page count, and navigation flags. Use @PageableDefault in controllers to set fallback values.
Java
// ── Service — build Pageable programmatically: ───────────────────────
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
public Page<UserResponse> findAll(int page, int size, String sortBy) {
Pageable pageable = PageRequest.of(
page, // zero-based page index
size, // page size
Sort.by(Sort.Direction.DESC, sortBy) // sort
);
return userRepository.findAll(pageable).map(UserResponse::from);
}
// ── Multi-column sort: ────────────────────────────────────────────
public Page<UserResponse> findAllMultiSort() {
Sort sort = Sort.by(
Sort.Order.asc("role"),
Sort.Order.desc("createdAt"),
Sort.Order.asc("name")
);
return userRepository.findAll(PageRequest.of(0, 20, sort))
.map(UserResponse::from);
}
// ── Sort without pagination: ──────────────────────────────────────
public List<UserResponse> findAllSorted() {
return userRepository.findAll(Sort.by("name"))
.stream().map(UserResponse::from).toList();
}
}
// ── Controller — Pageable from HTTP query parameters: ─────────────────
@GetMapping
public ResponseEntity<Page<UserResponse>> findAll(
@PageableDefault(size = 20, sort = "createdAt",
direction = Sort.Direction.DESC)
Pageable pageable) {
return ResponseEntity.ok(userService.findAll(pageable));
}
// GET /users → page 0, size 20, sort createdAt DESC
// GET /users?page=2&size=10 → page 2, size 10
// GET /users?sort=name,asc → sort by name ascending
// GET /users?sort=role,asc&sort=name,asc → multi-column sort
// ── Page<T> response structure: ───────────────────────────────────────
// {
// "content": [...],
// "pageable": { "pageNumber": 0, "pageSize": 20 },
// "totalElements": 148,
// "totalPages": 8,
// "first": true,
// "last": false,
// "size": 20,
// "number": 0,
// "numberOfElements": 20,
// "empty": false
// }
// ── Limit max page size globally: ─────────────────────────────────────
spring:
data:
web:
pageable:
max-page-size: 100
default-page-size: 20
one-indexed-parameters: false # keep 0-based (default)Projections — Selecting Specific Fields
Loading full entities when only a few fields are needed wastes memory and generates unnecessarily wide SELECT statements. Spring Data JPA projections let you define interfaces or classes that map query results to a subset of fields.
Java
// ── Interface projection — Spring generates an implementation: ────────
public interface UserSummary {
Long getId();
String getName();
String getEmail();
// Computed property — @Value with SpEL:
@Value("#{target.name + ' <' + target.email + '>'}")
String getDisplayName();
}
// ── Class projection (DTO) — constructor expression in JPQL: ──────────
public record UserDto(Long id, String name, String email) { }
// ── Repository — return projections directly: ─────────────────────────
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Interface projection — Spring generates SELECT id, name, email:
List<UserSummary> findByRole(User.Role role);
// Generic projection — caller specifies the projection type:
<T> List<T> findByRole(User.Role role, Class<T> type);
// DTO projection via @Query JPQL constructor expression:
@Query("SELECT new com.example.dto.UserDto(u.id, u.name, u.email) " +
"FROM User u WHERE u.role = :role")
List<UserDto> findDtosByRole(@Param("role") User.Role role);
// Interface projection with Pageable:
Page<UserSummary> findByActiveTrue(Pageable pageable);
}
// ── Usage — caller picks the projection: ──────────────────────────────
List<UserSummary> summaries = userRepository.findByRole(User.Role.ADMIN);
List<UserDto> dtos = userRepository.findByRole(
User.Role.ADMIN, UserDto.class);
// ── Dynamic projection — same method, different return shapes: ─────────
<T> Page<T> findByActiveTrue(Pageable pageable, Class<T> type);
// Call with full entity:
Page<User> full = repo.findByActiveTrue(pageable, User.class);
// Call with summary projection:
Page<UserSummary> summaries = repo.findByActiveTrue(pageable, UserSummary.class);Specifications — Dynamic Queries
JPA Specifications implement the Specification pattern for building type-safe, composable dynamic queries. They are the correct replacement for hand-built JPQL string concatenation when query conditions vary at runtime.
Java
// ── Enable Specifications — extend JpaSpecificationExecutor: ──────────
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> { }
// ── Define reusable Specification predicates: ─────────────────────────
public class UserSpecifications {
public static Specification<User> hasRole(User.Role role) {
return (root, query, cb) ->
role == null ? null : cb.equal(root.get("role"), role);
}
public static Specification<User> isActive(Boolean active) {
return (root, query, cb) ->
active == null ? null : cb.equal(root.get("active"), active);
}
public static Specification<User> nameContains(String search) {
return (root, query, cb) ->
search == null || search.isBlank() ? null :
cb.like(cb.lower(root.get("name")),
"%" + search.toLowerCase() + "%");
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
}
// ── Service — compose specifications at runtime: ──────────────────────
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
public Page<UserResponse> search(UserSearchRequest request, Pageable pageable) {
Specification<User> spec = Specification
.where(UserSpecifications.hasRole(request.role()))
.and(UserSpecifications.isActive(request.active()))
.and(UserSpecifications.nameContains(request.search()))
.and(UserSpecifications.createdAfter(request.createdAfter()));
return userRepository.findAll(spec, pageable).map(UserResponse::from);
}
}
// ── Request DTO: ───────────────────────────────────────────────────────
public record UserSearchRequest(
User.Role role, // null = all roles
Boolean active, // null = both active and inactive
String search, // null = no name filter
LocalDateTime createdAfter // null = no date filter
) { }Auditing with Spring Data
Spring Data JPA's auditing support automatically populates created/updated timestamps and the creating/modifying user on entity save — without @PrePersist/@PreUpdate lifecycle callbacks on every entity.
Java
// ── 1. Enable auditing on the main application class: ────────────────
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class MyApplication { }
// ── 2. Provide the current auditor (who is making the change): ────────
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication()
).map(Authentication::getName);
}
// ── 3. Define an auditable base class: ────────────────────────────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(length = 100)
private String updatedBy;
// getters
}
// ── 4. Extend entities from the base class: ───────────────────────────
@Entity
@Table(name = "products")
public class Product extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private BigDecimal price;
// Spring Data automatically sets createdAt, updatedAt,
// createdBy, updatedBy on save — no manual lifecycle callbacks needed.
}Custom Repository Implementations
When derived methods and @Query are insufficient — complex dynamic queries, bulk operations, Hibernate-specific APIs — you can add custom implementations to a repository by creating a fragment interface and its implementation. Spring Data merges the fragment into the main repository automatically.
Java
// ── 1. Fragment interface — declares the custom methods: ─────────────
public interface UserRepositoryCustom {
List<User> searchWithComplexCriteria(UserFilter filter);
int bulkUpdateStatus(List<Long> ids, String status);
}
// ── 2. Implementation — Impl suffix is required by convention: ─────────
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final EntityManager em;
@Override
public List<User> searchWithComplexCriteria(UserFilter filter) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (filter.getName() != null) {
predicates.add(cb.like(cb.lower(root.get("name")),
"%" + filter.getName().toLowerCase() + "%"));
}
if (filter.getRole() != null) {
predicates.add(cb.equal(root.get("role"), filter.getRole()));
}
if (filter.getMinAge() != null) {
predicates.add(cb.ge(root.get("age"), filter.getMinAge()));
}
query.where(predicates.toArray(new Predicate[0]));
query.orderBy(cb.desc(root.get("createdAt")));
return em.createQuery(query)
.setMaxResults(filter.getLimit())
.getResultList();
}
@Override
@Transactional
public int bulkUpdateStatus(List<Long> ids, String status) {
return em.createQuery(
"UPDATE User u SET u.status = :status WHERE u.id IN :ids")
.setParameter("status", status)
.setParameter("ids", ids)
.executeUpdate();
}
}
// ── 3. Main repository — extends both JpaRepository and the fragment: ─
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
UserRepositoryCustom {
// All JpaRepository methods + searchWithComplexCriteria + bulkUpdateStatus
// available through a single injection point.
}