Spring Boot Topic
Spring Data JPA
1. 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.
Key Benefits:
-
Reduced Boilerplate: Automatic repository implementation
-
Generated Queries: Query creation from method names
-
Pagination & Sorting: Built-in support
-
Auditing: Automatic entity tracking
-
Custom Queries: Flexible query definition
2. Entity Configuration
Basic Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true, length = 50)
private String username;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
// Constructors, getters, setters
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
this.createdAt = LocalDateTime.now();
}
// Getters and setters...
}
Entity Relationships
One-to-Many
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Employee> employees = new ArrayList<>();
// Helper method for bidirectional relationship
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
}
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
}
Many-to-Many
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
One-to-One
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private UserProfile profile;
}
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
3. Repository Interfaces
Basic Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Automatic CRUD operations are provided
}
Custom Repository with Query Methods
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods
List<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByUsernameContainingIgnoreCase(String username);
List<User> findByEmailEndingWith(String domain);
// Boolean operations
List<User> findByActiveTrue();
List<User> findByActiveFalse();
// Comparison operations
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeBetween(int start, int end);
List<User> findByCreatedAtAfter(LocalDateTime date);
// Multiple conditions
List<User> findByUsernameAndActive(String username, boolean active);
List<User> findByUsernameOrEmail(String username, String email);
// Sorting
List<User> findByActiveOrderByUsernameAsc(boolean active);
List<User> findByActiveOrderByCreatedAtDesc(boolean active);
// Limiting results
User findFirstByOrderByCreatedAtDesc();
List<User> findTop5ByOrderByAgeDesc();
// Count operations
long countByActive(boolean active);
boolean existsByEmail(String email);
// Delete operations
void deleteByActiveFalse();
long deleteByEmail(String email);
}
Custom Queries with @Query
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL Query
@Query("SELECT u FROM User u WHERE u.email = ?1")
Optional<User> findByEmailAddress(String email);
// JPQL with named parameters
@Query("SELECT u FROM User u WHERE u.username LIKE %:username% AND u.active = :active")
List<User> findByUsernameContainingAndActive(@Param("username") String username,
@Param("active") boolean active);
// Native SQL query
@Query(value = "SELECT * FROM users u WHERE u.created_at >= :date", nativeQuery = true)
List<User> findUsersCreatedAfter(@Param("date") LocalDateTime date);
// Complex join query
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.name = :roleName")
List<User> findByRoleName(@Param("roleName") String roleName);
// Projection query
@Query("SELECT u.username, u.email FROM User u WHERE u.active = true")
List<Object[]> findActiveUserDetails();
// Update query
@Modifying
@Query("UPDATE User u SET u.active = :active WHERE u.id = :id")
int updateUserStatus(@Param("id") Long id, @Param("active") boolean active);
// Delete query
@Modifying
@Query("DELETE FROM User u WHERE u.active = false AND u.createdAt < :date")
int deleteInactiveUsersBefore(@Param("date") LocalDateTime date);
}
4. Custom Repository Implementation
Custom Repository Interface
public interface CustomUserRepository {
List<User> findActiveUsersWithRecentActivity();
void updateLastLogin(Long userId, LocalDateTime loginTime);
}
public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository {
// Standard repository methods
}
Custom Repository Implementation
@Repository
public class CustomUserRepositoryImpl implements CustomUserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findActiveUsersWithRecentActivity() {
String jpql = """
SELECT u FROM User u
WHERE u.active = true
AND u.lastActivityAt >= :cutoffDate
ORDER BY u.lastActivityAt DESC
""";
return entityManager.createQuery(jpql, User.class)
.setParameter("cutoffDate", LocalDateTime.now().minusDays(30))
.getResultList();
}
@Override
public void updateLastLogin(Long userId, LocalDateTime loginTime) {
String jpql = "UPDATE User u SET u.lastLoginAt = :loginTime WHERE u.id = :userId";
entityManager.createQuery(jpql)
.setParameter("loginTime", loginTime)
.setParameter("userId", userId)
.executeUpdate();
}
}
5. Pagination and Sorting
public interface UserRepository extends JpaRepository<User, Long> {
// Pagination
Page<User> findByActive(boolean active, Pageable pageable);
// Sorting
List<User> findByActiveOrderByUsernameAsc(boolean active);
List<User> findByActive(boolean active, Sort sort);
// Slice for large datasets (no total count query)
Slice<User> findSliceByActive(boolean active, Pageable pageable);
}
// Usage in Service
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> getActiveUsers(int page, int size, String sortBy, String direction) {
Sort sort = direction.equalsIgnoreCase(Sort.Direction.ASC.name())
? Sort.by(sortBy).ascending()
: Sort.by(sortBy).descending();
Pageable pageable = PageRequest.of(page, size, sort);
return userRepository.findByActive(true, pageable);
}
public Page<User> searchUsers(String keyword, Pageable pageable) {
return userRepository.findByUsernameContainingOrEmailContaining(
keyword, keyword, pageable);
}
}
// Usage in Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<Page<User>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "username") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
Page<User> users = userService.getActiveUsers(page, size, sortBy, direction);
return ResponseEntity.ok(users);
}
}
6. Projections
Interface-based Projections
// Closed projection
public interface UserSummary {
String getUsername();
String getEmail();
LocalDateTime getCreatedAt();
// Default method in projection
default String getDisplayName() {
return getUsername() + " (" + getEmail() + ")";
}
}
// Open projection
public interface UserOpenProjection {
@Value("#{target.username + ' - ' + target.email}")
String getDisplayInfo();
}
// Using projections in repository
public interface UserRepository extends JpaRepository<User, Long> {
List<UserSummary> findByActiveTrue();
List<UserOpenProjection> findByEmailContaining(String email);
<T> List<T> findByActive(boolean active, Class<T> type);
}
Class-based Projections (DTO)
public class UserDTO {
private final String username;
private final String email;
private final long postCount;
public UserDTO(String username, String email, long postCount) {
this.username = username;
this.email = email;
this.postCount = postCount;
}
// getters...
}
// Using DTO projection in repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserDTO(u.username, u.email, COUNT(p)) " +
"FROM User u LEFT JOIN u.posts p WHERE u.active = true GROUP BY u.id")
List<UserDTO> findActiveUsersWithPostCount();
}
7. Auditing
Configuration
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.or(() -> Optional.of("system"));
}
}
Auditable Entity
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@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;
@Version
private Long version;
}
8. Specifications for Dynamic Queries
// Specification interface implementation
public class UserSpecifications {
public static Specification<User> hasUsername(String username) {
return (root, query, criteriaBuilder) ->
username == null ? null : criteriaBuilder.equal(root.get("username"), username);
}
public static Specification<User> isActive() {
return (root, query, criteriaBuilder) ->
criteriaBuilder.isTrue(root.get("active"));
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, criteriaBuilder) ->
date == null ? null : criteriaBuilder.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> hasEmailDomain(String domain) {
return (root, query, criteriaBuilder) ->
domain == null ? null : criteriaBuilder.like(root.get("email"), "%@" + domain);
}
}
// Repository extending JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
// Usage in Service
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(String username, Boolean active, LocalDateTime createdAfter, String emailDomain) {
Specification<User> spec = Specification.where(null);
if (username != null) {
spec = spec.and(UserSpecifications.hasUsername(username));
}
if (active != null) {
spec = spec.and(UserSpecifications.isActive());
}
if (createdAfter != null) {
spec = spec.and(UserSpecifications.createdAfter(createdAfter));
}
if (emailDomain != null) {
spec = spec.and(UserSpecifications.hasEmailDomain(emailDomain));
}
return userRepository.findAll(spec);
}
public Page<User> searchUsersWithPagination(Specification<User> spec, Pageable pageable) {
return userRepository.findAll(spec, pageable);
}
}
9. Transaction Management
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private AuditRepository auditRepository;
// Default transaction (REQUIRED)
public User createUser(User user) {
User savedUser = userRepository.save(user);
auditRepository.save(new Audit("USER_CREATED", savedUser.getId()));
return savedUser;
}
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUserStatus(Long userId, boolean active) {
User user = getUser(userId);
user.setActive(active);
userRepository.save(user);
}
@Transactional(rollbackFor = {ValidationException.class})
public User updateUserWithValidation(User user) {
if (user.getEmail() == null) {
throw new ValidationException("Email is required");
}
return userRepository.save(user);
}
@Transactional(timeout = 30) // 30 seconds timeout
public void batchUpdateUsers(List<User> users) {
for (User user : users) {
userRepository.save(user);
}
}
}
10. Entity Listeners and Callbacks
@Entity
@EntityListeners(UserEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String passwordHash;
@PrePersist
public void prePersist() {
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
hashPassword();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
hashPassword();
}
@PostLoad
public void postLoad() {
System.out.println("User loaded: " + this.username);
}
private void hashPassword() {
// Password hashing logic
}
}
// External Entity Listener
@Component
public class UserEntityListener {
@PrePersist
public void prePersist(User user) {
user.setCreatedAt(LocalDateTime.now());
}
@PreUpdate
public void preUpdate(User user) {
user.setUpdatedAt(LocalDateTime.now());
}
}
11. Testing
Repository Testing
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void whenFindByEmail_thenReturnUser() {
// given
User user = new User("john", "john@example.com");
entityManager.persist(user);
entityManager.flush();
// when
Optional<User> found = userRepository.findByEmail("john@example.com");
// then
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo("john");
}
@Test
void whenFindByNonExistentEmail_thenReturnEmpty() {
// when
Optional<User> found = userRepository.findByEmail("nonexistent@example.com");
// then
assertThat(found).isEmpty();
}
}
Integration Testing
@SpringBootTest
@Transactional
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void whenCreateUser_thenUserIsSaved() {
// given
User user = new User("testuser", "test@example.com");
// when
User saved = userService.createUser(user);
// then
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.count()).isEqualTo(1);
}
}
12. Configuration
application.properties
# DataSource Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=secret
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# Connection Pool
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.connection-timeout=20000
# Logging
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Java Configuration
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EnableTransactionManagement
public class JpaConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource());
em.setPackagesToScan("com.example.entity");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
Properties properties = new Properties();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", "true");
properties.put("hibernate.format_sql", "true");
em.setJpaProperties(properties);
return em;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}
This comprehensive guide covers Spring Data JPA from basic entity mapping to advanced features like specifications, projections, and auditing. Spring Data JPA significantly reduces the amount of boilerplate code needed for data access layers while providing powerful query capabilities and integration with Spring's ecosystem.
Comments (0)
No comments yet. Be the first to comment!