What is Spring Data JPA?
Spring Data JPA eliminates boilerplate code:
- Without Spring Data: Write 100+ lines for CRUD operations
- With Spring Data: Just define an interface, get implementation free!
Spring Data JPA is a layer on top of JPA (Java Persistence API) that provides:
- Repository interfaces with auto-generated implementations
- Query derivation from method names
- Custom JPQL and native SQL queries
- Pagination and sorting support
- Auditing capabilities
Setup
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Database driver (e.g., H2 for development) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Or PostgreSQL for production -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Entity Classes
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Enumerated(EnumType.STRING)
private Status status = Status.ACTIVE;
@Column(name = "created_at")
private LocalDateTime createdAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
@ManyToMany
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Constructors, getters, setters
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// getters/setters...
}
public enum Status {
ACTIVE, INACTIVE, SUSPENDED
}
Repository Interfaces
Basic Repository
import org.springframework.data.jpa.repository.JpaRepository;
// That's it! Spring Data provides the implementation
public interface UserRepository extends JpaRepository<User, Long> {
// Inherited methods:
// save(entity), saveAll(entities)
// findById(id), findAll(), findAllById(ids)
// count(), existsById(id)
// deleteById(id), delete(entity), deleteAll()
// findAll(Pageable), findAll(Sort)
}
Query Methods (Derived Queries)
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data generates queries from method names!
// SELECT * FROM users WHERE email = ?
Optional<User> findByEmail(String email);
// SELECT * FROM users WHERE name = ?
List<User> findByName(String name);
// SELECT * FROM users WHERE status = ?
List<User> findByStatus(Status status);
// SELECT * FROM users WHERE name LIKE '%value%'
List<User> findByNameContaining(String name);
// SELECT * FROM users WHERE name LIKE 'value%'
List<User> findByNameStartingWith(String prefix);
// SELECT * FROM users WHERE status = ? AND name = ?
List<User> findByStatusAndName(Status status, String name);
// SELECT * FROM users WHERE status = ? OR name = ?
List<User> findByStatusOrName(Status status, String name);
// SELECT * FROM users WHERE created_at BETWEEN ? AND ?
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
// SELECT * FROM users WHERE status IN (?, ?, ?)
List<User> findByStatusIn(Collection<Status> statuses);
// SELECT * FROM users WHERE email IS NOT NULL
List<User> findByEmailIsNotNull();
// SELECT * FROM users ORDER BY name ASC
List<User> findByStatusOrderByNameAsc(Status status);
// SELECT * FROM users LIMIT 1
Optional<User> findFirstByStatus(Status status);
// SELECT * FROM users LIMIT 5
List<User> findTop5ByStatus(Status status);
// SELECT COUNT(*) FROM users WHERE status = ?
long countByStatus(Status status);
// SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)
boolean existsByEmail(String email);
// DELETE FROM users WHERE status = ?
void deleteByStatus(Status status);
}
Custom JPQL Queries
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL query
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmailAddress(@Param("email") String email);
// JPQL with JOIN
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.name = :roleName")
List<User> findByRoleName(@Param("roleName") String roleName);
// JPQL with FETCH (eager loading)
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
Optional<User> findByEmailNative(String email);
// Projection (return specific fields)
@Query("SELECT u.name as name, u.email as email FROM User u")
List<UserSummary> findAllSummaries();
// Update query
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") Status status);
// Delete query
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteByStatusJpql(@Param("status") Status status);
}
// Projection interface
public interface UserSummary {
String getName();
String getEmail();
}
Pagination and Sorting
public interface UserRepository extends JpaRepository<User, Long> {
// Pageable support
Page<User> findByStatus(Status status, Pageable pageable);
// Sort support
List<User> findByStatus(Status status, Sort sort);
// Slice (like Page but without total count)
Slice<User> findByNameContaining(String name, Pageable pageable);
}
// Service using pagination
@Service
public class UserService {
private final UserRepository userRepository;
public Page<User> getUsers(int page, int size) {
// Page 0, size 10
Pageable pageable = PageRequest.of(page, size);
return userRepository.findAll(pageable);
}
public Page<User> getUsersSorted(int page, int size) {
// Sort by name ascending, then by createdAt descending
Pageable pageable = PageRequest.of(page, size,
Sort.by("name").ascending()
.and(Sort.by("createdAt").descending())
);
return userRepository.findAll(pageable);
}
public List<User> getActiveUsersSorted() {
// Just sorting, no pagination
return userRepository.findByStatus(
Status.ACTIVE,
Sort.by(Sort.Direction.DESC, "createdAt")
);
}
}
// Controller with pagination
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userService.findAll(pageable);
}
}
// Page response example:
// {
// "content": [...users...],
// "totalElements": 100,
// "totalPages": 10,
// "number": 0,
// "size": 10,
// "first": true,
// "last": false
// }
Specifications (Dynamic Queries)
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// Specification builder
public class UserSpecifications {
public static Specification<User> hasStatus(Status status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> hasRole(String roleName) {
return (root, query, cb) -> {
Join<User, Role> roles = root.join("roles");
return cb.equal(roles.get("name"), roleName);
};
}
}
// Using specifications
@Service
public class UserSearchService {
private final UserRepository userRepository;
public List<User> search(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(null);
if (criteria.getStatus() != null) {
spec = spec.and(UserSpecifications.hasStatus(criteria.getStatus()));
}
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.nameLike(criteria.getName()));
}
if (criteria.getCreatedAfter() != null) {
spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
}
return userRepository.findAll(spec);
}
}
Auditing
// Enable auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
// Base auditable entity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
// getters/setters
}
// Entity extending Auditable
@Entity
public class User extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// other fields...
}
// Provide current user for @CreatedBy/@LastModifiedBy
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Get from Spring Security context
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
Best Practices
DO:
- Use Optional for single results - findById returns Optional
- Use pagination for large datasets - Page or Slice
- Use projections for partial data - Better performance
- Use @Transactional on service layer - Not repository
- Use fetch joins for N+1 queries - LEFT JOIN FETCH
- Use specifications for dynamic queries - Composable
DON'T:
- Don't return entities to controllers - Use DTOs
- Don't use findAll() without limits - Use pagination
- Don't forget @Modifying - Required for UPDATE/DELETE
- Don't ignore lazy loading - Use eager fetch when needed
- Don't use native queries if JPQL works - Less portable
Summary
- Spring Data JPA: Simplifies JPA data access
- JpaRepository: CRUD + pagination + sorting
- Query Methods: findByX, countByX, deleteByX
- @Query: Custom JPQL or native SQL
- Pageable: Pagination with PageRequest.of()
- Sort: Sorting with Sort.by()
- Specifications: Dynamic, composable queries
- Auditing: @CreatedDate, @LastModifiedDate
- Projections: Return specific fields only