Menu

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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?