Introduction to Spring Data JPA
Spring Data JPA is part of the larger Spring Data family that makes it easy to implement JPA-based repositories. It reduces the amount of boilerplate code required to implement data access layers for both relational and non-relational databases.
Key Benefits
- Reduced Boilerplate: Automatic repository implementation
- Built-in CRUD Operations: Common operations out of the box
- Dynamic Query Generation: From method names
- Pagination and Sorting: Built-in support
- Integration with Spring Ecosystem: Seamless Spring integration
Project Setup
Maven Dependencies
<dependencies> <!-- Spring Boot Starter Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Database Driver (H2 for example) --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Alternatively, for PostgreSQL --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <!-- Spring Boot Starter Web (for REST controllers) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
Application Configuration
# application.properties # H2 Database Configuration spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= # JPA Configuration spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # H2 Console (for development) spring.h2.console.enabled=true # For PostgreSQL # spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase # spring.datasource.username=postgres # spring.datasource.password=password # spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # spring.jpa.hibernate.ddl-auto=update
Entity Modeling
Basic Entity
package com.example.entity; import javax.persistence.*; import java.time.LocalDateTime; import java.util.Objects; @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", unique = true, nullable = false, length = 50) private String username; @Column(name = "email", unique = true, nullable = false) private String email; @Column(name = "first_name", nullable = false, length = 50) private String firstName; @Column(name = "last_name", nullable = false, length = 50) private String lastName; @Column(name = "age") private Integer age; @Enumerated(EnumType.STRING) @Column(name = "status") private UserStatus status = UserStatus.ACTIVE; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; // Default constructor (required by JPA) public User() {} // Constructor for required fields public User(String username, String email, String firstName, String lastName) { this.username = username; this.email = email; this.firstName = firstName; this.lastName = lastName; this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } // Pre-persist and pre-update callbacks @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public UserStatus getStatus() { return status; } public void setStatus(UserStatus status) { this.status = status; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } // equals and hashCode @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; User user = (User) o; return Objects.equals(id, user.id) && Objects.equals(username, user.username); } @Override public int hashCode() { return Objects.hash(id, username); } // toString @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", age=" + age + ", status=" + status + '}'; } } enum UserStatus { ACTIVE, INACTIVE, SUSPENDED } Entity Relationships
package com.example.entity; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity @Table(name = "departments") public class Department { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @Column(name = "description") private String description; // One-to-Many relationship with Employee @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Employee> employees = new ArrayList<>(); // Constructors public Department() {} public Department(String name, String description) { this.name = name; this.description = description; } // Helper methods for bidirectional relationship public void addEmployee(Employee employee) { employees.add(employee); employee.setDepartment(this); } public void removeEmployee(Employee employee) { employees.remove(employee); employee.setDepartment(null); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public List<Employee> getEmployees() { return employees; } public void setEmployees(List<Employee> employees) { this.employees = employees; } } @Entity @Table(name = "employees") public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "employee_id", unique = true, nullable = false) private String employeeId; @Column(name = "first_name", nullable = false) private String firstName; @Column(name = "last_name", nullable = false) private String lastName; @Column(name = "email") private String email; @Column(name = "salary") private Double salary; // Many-to-One relationship with Department @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "department_id") private Department department; // Many-to-Many relationship with Project @ManyToMany @JoinTable( name = "employee_project", joinColumns = @JoinColumn(name = "employee_id"), inverseJoinColumns = @JoinColumn(name = "project_id") ) private List<Project> projects = new ArrayList<>(); // Constructors public Employee() {} public Employee(String employeeId, String firstName, String lastName, String email) { this.employeeId = employeeId; this.firstName = firstName; this.lastName = lastName; this.email = email; } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getEmployeeId() { return employeeId; } public void setEmployeeId(String employeeId) { this.employeeId = employeeId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Double getSalary() { return salary; } public void setSalary(Double salary) { this.salary = salary; } public Department getDepartment() { return department; } public void setDepartment(Department department) { this.department = department; } public List<Project> getProjects() { return projects; } public void setProjects(List<Project> projects) { this.projects = projects; } } @Entity @Table(name = "projects") public class Project { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", nullable = false) private String name; @Column(name = "description") private String description; @Column(name = "budget") private Double budget; // Many-to-Many relationship with Employee (inverse side) @ManyToMany(mappedBy = "projects") private List<Employee> employees = new ArrayList<>(); // Constructors, Getters and Setters public Project() {} public Project(String name, String description, Double budget) { this.name = name; this.description = description; this.budget = budget; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Double getBudget() { return budget; } public void setBudget(Double budget) { this.budget = budget; } public List<Employee> getEmployees() { return employees; } public void setEmployees(List<Employee> employees) { this.employees = employees; } } Repository Interfaces
Basic Repository Interface
package com.example.repository; import com.example.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Long> { // Basic CRUD operations are inherited } Custom Repository with Query Methods
package com.example.repository; import com.example.entity.User; import com.example.entity.UserStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository<User, Long> { // Derived Query Methods (automatically implemented by Spring Data JPA) // Find by exact match Optional<User> findByUsername(String username); Optional<User> findByEmail(String email); // Find by multiple conditions List<User> findByFirstNameAndLastName(String firstName, String lastName); List<User> findByFirstNameOrLastName(String firstName, String lastName); // Find with comparison operators List<User> findByAgeGreaterThan(int age); List<User> findByAgeLessThanEqual(int age); List<User> findByAgeBetween(int startAge, int endAge); // Find with null checks List<User> findByLastNameIsNull(); List<User> findByLastNameIsNotNull(); // Find with like/contains List<User> findByFirstNameContaining(String firstName); List<User> findByFirstNameLike(String pattern); List<User> findByFirstNameStartingWith(String prefix); List<User> findByFirstNameEndingWith(String suffix); // Find with ordering List<User> findByStatusOrderByFirstNameAsc(UserStatus status); List<User> findByStatusOrderByCreatedAtDesc(UserStatus status); // Find with limit List<User> findTop5ByOrderByCreatedAtDesc(); List<User> findFirst10ByStatus(UserStatus status); // Count operations long countByStatus(UserStatus status); boolean existsByUsername(String username); boolean existsByEmail(String email); // Delete operations void deleteByStatus(UserStatus status); long deleteByLastName(String lastName); // Custom JPQL Queries @Query("SELECT u FROM User u WHERE u.email LIKE '%@gmail.com'") List<User> findGmailUsers(); @Query("SELECT u FROM User u WHERE u.firstName = :firstName AND u.age > :age") List<User> findUsersByFirstNameAndMinAge(@Param("firstName") String firstName, @Param("age") int minAge); @Query("SELECT u.firstName, u.lastName FROM User u WHERE u.status = :status") List<Object[]> findUserNamesByStatus(@Param("status") UserStatus status); // Native SQL Query @Query(value = "SELECT * FROM users u WHERE u.created_at > :date", nativeQuery = true) List<User> findUsersCreatedAfter(@Param("date") LocalDateTime date); // Update query @Modifying @Query("UPDATE User u SET u.status = :status WHERE u.lastLogin < :date") int deactivateInactiveUsers(@Param("status") UserStatus status, @Param("date") LocalDateTime date); // Complex query with aggregation @Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status") List<Object[]> countUsersByStatus(); } Employee Repository with Relationships
package com.example.repository; import com.example.entity.Department; import com.example.entity.Employee; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface EmployeeRepository extends JpaRepository<Employee, Long> { // Basic queries Optional<Employee> findByEmployeeId(String employeeId); List<Employee> findByFirstNameContainingIgnoreCase(String firstName); List<Employee> findByLastNameContainingIgnoreCase(String lastName); // Relationship queries List<Employee> findByDepartment(Department department); List<Employee> findByDepartmentName(String departmentName); // Salary-based queries List<Employee> findBySalaryGreaterThan(Double salary); List<Employee> findBySalaryBetween(Double minSalary, Double maxSalary); // Complex queries with joins @Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName AND e.salary > :minSalary") List<Employee> findEmployeesInDepartmentWithMinSalary(@Param("deptName") String departmentName, @Param("minSalary") Double minSalary); // Project count for employees @Query("SELECT e, SIZE(e.projects) FROM Employee e") List<Object[]> findEmployeesWithProjectCount(); // Employees working on specific project @Query("SELECT e FROM Employee e JOIN e.projects p WHERE p.name = :projectName") List<Employee> findEmployeesByProjectName(@Param("projectName") String projectName); // Average salary by department @Query("SELECT d.name, AVG(e.salary) FROM Employee e JOIN e.department d GROUP BY d.name") List<Object[]> findAverageSalaryByDepartment(); } Service Layer
User Service
package com.example.service; import com.example.entity.User; import com.example.entity.UserStatus; import com.example.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Service @Transactional public class UserService { @Autowired private UserRepository userRepository; // Basic CRUD operations public User createUser(User user) { return userRepository.save(user); } public Optional<User> getUserById(Long id) { return userRepository.findById(id); } public List<User> getAllUsers() { return userRepository.findAll(); } public User updateUser(User user) { if (!userRepository.existsById(user.getId())) { throw new RuntimeException("User not found with id: " + user.getId()); } return userRepository.save(user); } public void deleteUser(Long id) { userRepository.deleteById(id); } // Custom business logic methods public Optional<User> getUserByUsername(String username) { return userRepository.findByUsername(username); } public Optional<User> getUserByEmail(String email) { return userRepository.findByEmail(email); } public List<User> getUsersByStatus(UserStatus status) { return userRepository.findByStatus(status); } public List<User> searchUsersByName(String name) { return userRepository.findByFirstNameContainingOrLastNameContaining(name, name); } public List<User> getUsersByAgeRange(int minAge, int maxAge) { return userRepository.findByAgeBetween(minAge, maxAge); } public long countActiveUsers() { return userRepository.countByStatus(UserStatus.ACTIVE); } public boolean usernameExists(String username) { return userRepository.existsByUsername(username); } public boolean emailExists(String email) { return userRepository.existsByEmail(email); } // Pagination and sorting public Page<User> getUsers(Pageable pageable) { return userRepository.findAll(pageable); } public Page<User> getActiveUsers(Pageable pageable) { return userRepository.findByStatus(UserStatus.ACTIVE, pageable); } // Bulk operations @Transactional public void deactivateInactiveUsers(LocalDateTime cutoffDate) { userRepository.deactivateInactiveUsers(UserStatus.INACTIVE, cutoffDate); } // Complex business logic @Transactional public User registerUser(User user) { if (usernameExists(user.getUsername())) { throw new RuntimeException("Username already exists: " + user.getUsername()); } if (emailExists(user.getEmail())) { throw new RuntimeException("Email already exists: " + user.getEmail()); } user.setStatus(UserStatus.ACTIVE); return userRepository.save(user); } @Transactional public void deactivateUser(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found with id: " + userId)); user.setStatus(UserStatus.INACTIVE); userRepository.save(user); } } Department Service with Relationships
package com.example.service; import com.example.entity.Department; import com.example.entity.Employee; import com.example.repository.DepartmentRepository; import com.example.repository.EmployeeRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service @Transactional public class DepartmentService { @Autowired private DepartmentRepository departmentRepository; @Autowired private EmployeeRepository employeeRepository; public Department createDepartment(Department department) { return departmentRepository.save(department); } public Optional<Department> getDepartmentById(Long id) { return departmentRepository.findById(id); } public List<Department> getAllDepartments() { return departmentRepository.findAll(); } public Department updateDepartment(Department department) { if (!departmentRepository.existsById(department.getId())) { throw new RuntimeException("Department not found with id: " + department.getId()); } return departmentRepository.save(department); } public void deleteDepartment(Long id) { departmentRepository.deleteById(id); } // Relationship management @Transactional public void addEmployeeToDepartment(Long departmentId, Employee employee) { Department department = departmentRepository.findById(departmentId) .orElseThrow(() -> new RuntimeException("Department not found")); department.addEmployee(employee); departmentRepository.save(department); } @Transactional public void removeEmployeeFromDepartment(Long departmentId, Long employeeId) { Department department = departmentRepository.findById(departmentId) .orElseThrow(() -> new RuntimeException("Department not found")); Employee employee = employeeRepository.findById(employeeId) .orElseThrow(() -> new RuntimeException("Employee not found")); department.removeEmployee(employee); departmentRepository.save(department); } public List<Employee> getEmployeesInDepartment(Long departmentId) { Department department = departmentRepository.findById(departmentId) .orElseThrow(() -> new RuntimeException("Department not found")); return department.getEmployees(); } // Business logic public Optional<Department> getDepartmentByName(String name) { return departmentRepository.findByName(name); } public List<Object[]> getDepartmentStatistics() { return departmentRepository.getDepartmentStatistics(); } } REST Controllers
User Controller
package com.example.controller; import com.example.entity.User; import com.example.entity.UserStatus; import com.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; import java.util.Optional; @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public ResponseEntity<List<User>> getAllUsers() { List<User> users = userService.getAllUsers(); return ResponseEntity.ok(users); } @GetMapping("/paged") public ResponseEntity<Page<User>> getUsersPaged( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "asc") String sortDirection) { Sort sort = sortDirection.equalsIgnoreCase("desc") ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); Pageable pageable = PageRequest.of(page, size, sort); Page<User> users = userService.getUsers(pageable); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { Optional<User> user = userService.getUserById(id); return user.map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/username/{username}") public ResponseEntity<User> getUserByUsername(@PathVariable String username) { Optional<User> user = userService.getUserByUsername(username); return user.map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/status/{status}") public ResponseEntity<List<User>> getUsersByStatus(@PathVariable UserStatus status) { List<User> users = userService.getUsersByStatus(status); return ResponseEntity.ok(users); } @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { try { User createdUser = userService.registerUser(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } catch (RuntimeException e) { return ResponseEntity.badRequest().build(); } } @PutMapping("/{id}") public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) { user.setId(id); try { User updatedUser = userService.updateUser(user); return ResponseEntity.ok(updatedUser); } catch (RuntimeException e) { return ResponseEntity.notFound().build(); } } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { if (userService.getUserById(id).isPresent()) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); } @PatchMapping("/{id}/deactivate") public ResponseEntity<Void> deactivateUser(@PathVariable Long id) { try { userService.deactivateUser(id); return ResponseEntity.ok().build(); } catch (RuntimeException e) { return ResponseEntity.notFound().build(); } } @GetMapping("/search") public ResponseEntity<List<User>> searchUsers(@RequestParam String name) { List<User> users = userService.searchUsersByName(name); return ResponseEntity.ok(users); } @GetMapping("/count/active") public ResponseEntity<Long> countActiveUsers() { long count = userService.countActiveUsers(); return ResponseEntity.ok(count); } } Advanced Features
Custom Repository Implementation
// Custom repository interface package com.example.repository; import com.example.entity.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; public interface CustomUserRepository { List<User> findUsersWithComplexCriteria(String name, Integer minAge, Integer maxAge); Page<User> findUsersWithPagination(String searchTerm, Pageable pageable); } // Implementation package com.example.repository.impl; import com.example.entity.User; import com.example.repository.CustomUserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.util.ArrayList; import java.util.List; @Repository public class CustomUserRepositoryImpl implements CustomUserRepository { @PersistenceContext private EntityManager entityManager; @Override public List<User> findUsersWithComplexCriteria(String name, Integer minAge, Integer maxAge) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); List<Predicate> predicates = new ArrayList<>(); if (name != null && !name.isEmpty()) { Predicate firstNamePredicate = cb.like(user.get("firstName"), "%" + name + "%"); Predicate lastNamePredicate = cb.like(user.get("lastName"), "%" + name + "%"); predicates.add(cb.or(firstNamePredicate, lastNamePredicate)); } if (minAge != null) { predicates.add(cb.greaterThanOrEqualTo(user.get("age"), minAge)); } if (maxAge != null) { predicates.add(cb.lessThanOrEqualTo(user.get("age"), maxAge)); } query.where(predicates.toArray(new Predicate[0])); return entityManager.createQuery(query).getResultList(); } @Override public Page<User> findUsersWithPagination(String searchTerm, Pageable pageable) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> user = query.from(User.class); if (searchTerm != null && !searchTerm.isEmpty()) { Predicate searchPredicate = cb.or( cb.like(cb.lower(user.get("firstName")), "%" + searchTerm.toLowerCase() + "%"), cb.like(cb.lower(user.get("lastName")), "%" + searchTerm.toLowerCase() + "%"), cb.like(cb.lower(user.get("email")), "%" + searchTerm.toLowerCase() + "%") ); query.where(searchPredicate); } TypedQuery<User> typedQuery = entityManager.createQuery(query); typedQuery.setFirstResult((int) pageable.getOffset()); typedQuery.setMaxResults(pageable.getPageSize()); List<User> result = typedQuery.getResultList(); // Get total count for pagination CriteriaQuery<Long> countQuery = cb.createQuery(Long.class); Root<User> countRoot = countQuery.from(User.class); countQuery.select(cb.count(countRoot)); if (searchTerm != null && !searchTerm.isEmpty()) { Predicate searchPredicate = cb.or( cb.like(cb.lower(countRoot.get("firstName")), "%" + searchTerm.toLowerCase() + "%"), cb.like(cb.lower(countRoot.get("lastName")), "%" + searchTerm.toLowerCase() + "%"), cb.like(cb.lower(countRoot.get("email")), "%" + searchTerm.toLowerCase() + "%") ); countQuery.where(searchPredicate); } Long total = entityManager.createQuery(countQuery).getSingleResult(); return new PageImpl<>(result, pageable, total); } } // Extend the main repository interface public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository { // ... other methods } Auditing with Spring Data JPA
// Configuration @Configuration @EnableJpaAuditing public class AuditingConfig { @Bean public AuditorAware<String> auditorProvider() { return new SpringSecurityAuditorAware(); } } // AuditorAware implementation @Component public class SpringSecurityAuditorAware implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { // Get current username from Spring Security Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { return Optional.empty(); } return Optional.of(authentication.getName()); } } // Auditable entity @EntityListeners(AuditingEntityListener.class) @MappedSuperclass public abstract class Auditable { @CreatedDate @Column(name = "created_date") private LocalDateTime createdDate; @LastModifiedDate @Column(name = "last_modified_date") private LocalDateTime lastModifiedDate; @CreatedBy @Column(name = "created_by") private String createdBy; @LastModifiedBy @Column(name = "last_modified_by") private String lastModifiedBy; // Getters and Setters } Testing
package com.example.repository; import com.example.entity.User; import com.example.entity.UserStatus; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @DataJpaTest public class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test public void testSaveUser() { User user = new User("testuser", "[email protected]", "John", "Doe"); user.setAge(30); User savedUser = userRepository.save(user); assertNotNull(savedUser.getId()); assertEquals("testuser", savedUser.getUsername()); } @Test public void testFindByUsername() { User user = new User("testuser", "[email protected]", "John", "Doe"); userRepository.save(user); Optional<User> foundUser = userRepository.findByUsername("testuser"); assertTrue(foundUser.isPresent()); assertEquals("John", foundUser.get().getFirstName()); } @Test public void testFindByAgeGreaterThan() { User user1 = new User("user1", "[email protected]", "Alice", "Smith"); user1.setAge(25); User user2 = new User("user2", "[email protected]", "Bob", "Johnson"); user2.setAge(35); userRepository.saveAll(List.of(user1, user2)); List<User> users = userRepository.findByAgeGreaterThan(30); assertEquals(1, users.size()); assertEquals("Bob", users.get(0).getFirstName()); } @Test public void testPagination() { // Create test data for (int i = 1; i <= 15; i++) { User user = new User("user" + i, "user" + i + "@example.com", "User" + i, "Test"); userRepository.save(user); } PageRequest pageable = PageRequest.of(0, 10, Sort.by("username").ascending()); Page<User> page = userRepository.findAll(pageable); assertEquals(15, page.getTotalElements()); assertEquals(2, page.getTotalPages()); assertEquals(10, page.getContent().size()); } @Test public void testCustomQuery() { User user = new User("testuser", "[email protected]", "John", "Doe"); userRepository.save(user); List<User> gmailUsers = userRepository.findGmailUsers(); assertFalse(gmailUsers.isEmpty()); assertTrue(gmailUsers.get(0).getEmail().endsWith("@gmail.com")); } } Best Practices
- Use Lazy Loading for relationships to avoid N+1 query problems
- Implement equals() and hashCode() correctly for entities
- Use @Transactional appropriately in service layer
- Avoid Entity in DTO - use separate DTO classes for API
- Use Pagination for large datasets
- Implement Proper Error Handling
- Use Projections for read-only operations
- Monitor SQL Queries in development
- Use Connection Pooling in production
- Implement Caching where appropriate
This comprehensive guide covers the fundamentals of Spring Data JPA, from basic entity mapping to advanced features like custom repositories, auditing, and testing.