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.