1. java
  2. /spring
  3. /spring-data

Master Spring Data and Repository Pattern

Spring Data

Spring Data is a powerful umbrella project that simplifies data access across different data stores. It provides a consistent programming model for data access while retaining the special traits of each data store. Spring Data reduces boilerplate code and provides automatic implementation of common data access patterns.

Spring Data Overview

Spring Data provides several modules for different data stores:

Key Modules:

  • Spring Data JPA: Relational databases using JPA
  • Spring Data MongoDB: Document-oriented MongoDB
  • Spring Data Redis: Key-value store Redis
  • Spring Data Elasticsearch: Search engine Elasticsearch
  • Spring Data Neo4j: Graph database Neo4j

Core Benefits:

  • Reduced Boilerplate: Automatic repository implementations
  • Query Derivation: Generate queries from method names
  • Custom Queries: Support for custom JPQL, SQL, and native queries
  • Pagination & Sorting: Built-in pagination and sorting support
  • Auditing: Automatic auditing of entity changes
  • Specifications: Type-safe criteria queries
// Traditional DAO approach (lots of boilerplate)
@Repository
public class UserDaoImpl implements UserDao {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<User> findAll() {
        return entityManager.createQuery("SELECT u FROM User u", User.class)
                           .getResultList();
    }
    
    @Override
    public Optional<User> findById(Long id) {
        User user = entityManager.find(User.class, id);
        return Optional.ofNullable(user);
    }
    
    @Override
    public User save(User user) {
        if (user.getId() == null) {
            entityManager.persist(user);
            return user;
        } else {
            return entityManager.merge(user);
        }
    }
    
    @Override
    public void deleteById(Long id) {
        User user = entityManager.find(User.class, id);
        if (user != null) {
            entityManager.remove(user);
        }
    }
    
    @Override
    public List<User> findByEmail(String email) {
        return entityManager.createQuery(
            "SELECT u FROM User u WHERE u.email = :email", User.class)
            .setParameter("email", email)
            .getResultList();
    }
}

// Spring Data JPA approach (minimal code)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Basic CRUD operations provided automatically
    
    // Query derived from method name
    Optional<User> findByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);
    List<User> findByAgeBetween(Integer minAge, Integer maxAge);
    
    // Custom query
    @Query("SELECT u FROM User u WHERE u.active = true")
    List<User> findActiveUsers();
}

Repository Interfaces

Spring Data provides a hierarchy of repository interfaces:

Repository Hierarchy

// 1. Repository - marker interface
public interface Repository<T, ID> {
    // No methods - just a marker
}

// 2. CrudRepository - basic CRUD operations
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    long count();
    void deleteById(ID id);
    void delete(T entity);
    void deleteAllById(Iterable<? extends ID> ids);
    void deleteAll(Iterable<? extends T> entities);
    void deleteAll();
}

// 3. PagingAndSortingRepository - adds pagination and sorting
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
}

// 4. JpaRepository - JPA specific operations
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAllById(Iterable<ID> ids);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
    void deleteAllInBatch(Iterable<T> entities);
    void deleteAllByIdInBatch(Iterable<ID> ids);
    void deleteAllInBatch();
    T getOne(ID id);
    T getById(ID id);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}

Custom Repository Example

// User entity
@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    private Integer age;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "last_login")
    private LocalDateTime lastLogin;
    
    private boolean active = true;
    
    @Enumerated(EnumType.STRING)
    private UserRole role;
    
    // Constructors, getters, and setters
    public User() {}
    
    public User(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.createdAt = LocalDateTime.now();
    }
    
    // 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 getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    public LocalDateTime getLastLogin() { return lastLogin; }
    public void setLastLogin(LocalDateTime lastLogin) { this.lastLogin = lastLogin; }
    
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
    
    public UserRole getRole() { return role; }
    public void setRole(UserRole role) { this.role = role; }
}

public enum UserRole {
    USER, ADMIN, MODERATOR
}

// Repository with various query methods
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Query derived from method name
    Optional<User> findByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);
    List<User> findByAgeBetween(Integer minAge, Integer maxAge);
    List<User> findByRole(UserRole role);
    List<User> findByActiveTrue();
    List<User> findByCreatedAtAfter(LocalDateTime date);
    
    // Multiple conditions
    List<User> findByActiveAndRole(boolean active, UserRole role);
    List<User> findByNameContainingAndAgeGreaterThan(String name, Integer age);
    
    // Ordering
    List<User> findByActiveOrderByCreatedAtDesc(boolean active);
    List<User> findByRoleOrderByNameAsc(UserRole role);
    
    // Limiting results
    List<User> findTop10ByActiveOrderByCreatedAtDesc(boolean active);
    User findFirstByOrderByCreatedAtDesc();
    
    // Counting and existence
    long countByActive(boolean active);
    boolean existsByEmail(String email);
    
    // Custom JPQL queries
    @Query("SELECT u FROM User u WHERE u.lastLogin < :cutoffDate")
    List<User> findInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
    
    @Query("SELECT u FROM User u WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%')) " +
           "OR LOWER(u.email) LIKE LOWER(CONCAT('%', :search, '%'))")
    List<User> searchUsers(@Param("search") String searchTerm);
    
    // Native SQL query
    @Query(value = "SELECT * FROM users WHERE age > ?1 AND created_at > ?2", nativeQuery = true)
    List<User> findUsersWithNativeQuery(Integer age, LocalDateTime createdAfter);
    
    // Modifying queries
    @Modifying
    @Query("UPDATE User u SET u.lastLogin = :loginTime WHERE u.id = :userId")
    int updateLastLogin(@Param("userId") Long userId, @Param("loginTime") LocalDateTime loginTime);
    
    @Modifying
    @Query("DELETE FROM User u WHERE u.active = false AND u.lastLogin < :cutoffDate")
    int deleteInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
    
    // Pagination and sorting
    Page<User> findByActive(boolean active, Pageable pageable);
    Slice<User> findByRole(UserRole role, Pageable pageable);
}

Query Methods and Derived Queries

Spring Data automatically implements queries based on method names:

Query Keywords

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    // Equality
    List<Product> findByName(String name);
    List<Product> findByNameIs(String name);
    List<Product> findByNameEquals(String name);
    
    // Negation
    List<Product> findByNameNot(String name);
    List<Product> findByActiveIsNot(boolean active);
    
    // Null checks
    List<Product> findByDescriptionIsNull();
    List<Product> findByDescriptionIsNotNull();
    
    // String operations
    List<Product> findByNameStartingWith(String prefix);
    List<Product> findByNameEndingWith(String suffix);
    List<Product> findByNameContaining(String substring);
    List<Product> findByNameLike(String pattern);
    List<Product> findByNameNotLike(String pattern);
    List<Product> findByNameIgnoreCase(String name);
    
    // Numerical comparisons
    List<Product> findByPriceGreaterThan(BigDecimal price);
    List<Product> findByPriceLessThan(BigDecimal price);
    List<Product> findByPriceGreaterThanEqual(BigDecimal price);
    List<Product> findByPriceLessThanEqual(BigDecimal price);
    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
    
    // Date/Time operations
    List<Product> findByCreatedDateAfter(LocalDateTime date);
    List<Product> findByCreatedDateBefore(LocalDateTime date);
    
    // Collections
    List<Product> findByTagsIn(Collection<String> tags);
    List<Product> findByTagsNotIn(Collection<String> tags);
    List<Product> findByTagsContaining(String tag);
    
    // Boolean operations
    List<Product> findByActiveTrue();
    List<Product> findByActiveFalse();
    
    // Combining conditions
    List<Product> findByNameAndActive(String name, boolean active);
    List<Product> findByNameOrDescription(String name, String description);
    
    // Ordering
    List<Product> findByActiveOrderByNameAsc(boolean active);
    List<Product> findByActiveOrderByPriceDescNameAsc(boolean active);
    
    // Limiting
    List<Product> findTop10ByActiveOrderByCreatedDateDesc(boolean active);
    Product findFirstByOrderByPriceDesc();
    
    // Distinct
    List<Product> findDistinctByCategory(String category);
    
    // Counting
    long countByActive(boolean active);
    long countByCategoryAndActive(String category, boolean active);
    
    // Existence
    boolean existsByName(String name);
    boolean existsByCategoryAndActive(String category, boolean active);
    
    // Deletion
    void deleteByActive(boolean active);
    long deleteByCreatedDateBefore(LocalDateTime date);
}

Advanced Query Examples

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    // Complex nested property queries
    List<Order> findByCustomerNameContaining(String customerName);
    List<Order> findByCustomerAddressCity(String city);
    List<Order> findByOrderItemsProductName(String productName);
    
    // Multiple joins
    @Query("SELECT DISTINCT o FROM Order o " +
           "JOIN o.customer c " +
           "JOIN o.orderItems oi " +
           "JOIN oi.product p " +
           "WHERE c.active = true AND p.category = :category")
    List<Order> findActiveCustomerOrdersByProductCategory(@Param("category") String category);
    
    // Aggregation queries
    @Query("SELECT o.status, COUNT(o) FROM Order o GROUP BY o.status")
    List<Object[]> countOrdersByStatus();
    
    @Query("SELECT c.name, SUM(o.total) FROM Order o JOIN o.customer c " +
           "GROUP BY c.id, c.name ORDER BY SUM(o.total) DESC")
    List<Object[]> findTopCustomersByTotalSpent();
    
    // Subqueries
    @Query("SELECT o FROM Order o WHERE o.total > " +
           "(SELECT AVG(ord.total) FROM Order ord)")
    List<Order> findOrdersAboveAverageTotal();
    
    // Window functions (PostgreSQL example)
    @Query(value = "SELECT *, " +
                   "ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) as rn " +
                   "FROM orders WHERE rn = 1", nativeQuery = true)
    List<Order> findLatestOrderPerCustomer();
    
    // Date functions
    @Query("SELECT o FROM Order o WHERE YEAR(o.createdAt) = :year")
    List<Order> findOrdersByYear(@Param("year") int year);
    
    @Query("SELECT o FROM Order o WHERE DATE(o.createdAt) = CURRENT_DATE")
    List<Order> findTodaysOrders();
    
    // Projection queries
    @Query("SELECT new com.example.dto.OrderSummary(o.id, o.orderNumber, o.total, c.name) " +
           "FROM Order o JOIN o.customer c")
    List<OrderSummary> findOrderSummaries();
    
    // Custom result mapping
    @Query("SELECT o.status as status, COUNT(o) as count " +
           "FROM Order o GROUP BY o.status")
    List<StatusCount> getStatusCounts();
    
    interface StatusCount {
        String getStatus();
        Long getCount();
    }
}

Pagination and Sorting

Spring Data provides excellent support for pagination and sorting:

Basic Pagination

@Service
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public Page<User> getUsers(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findAll(pageable);
    }
    
    public Page<User> getUsersWithSorting(int page, int size, String sortBy, String sortDir) {
        Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        return userRepository.findAll(pageable);
    }
    
    public Page<User> getActiveUsers(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return userRepository.findByActive(true, pageable);
    }
    
    public Slice<User> getUserSlice(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findByRole(UserRole.USER, pageable);
    }
}

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping
    public ResponseEntity<Page<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir) {
        
        Page<User> users = userService.getUsersWithSorting(page, size, sortBy, sortDir);
        return ResponseEntity.ok(users);
    }
    
    @GetMapping("/active")
    public ResponseEntity<Page<User>> getActiveUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Page<User> users = userService.getActiveUsers(page, size);
        return ResponseEntity.ok(users);
    }
}

Advanced Sorting

@Service
public class ProductService {
    
    private final ProductRepository productRepository;
    
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
    
    public Page<Product> getProducts(ProductSearchCriteria criteria) {
        // Multiple sort fields
        Sort sort = Sort.by(
            Sort.Order.desc("featured"),
            Sort.Order.asc("price"),
            Sort.Order.asc("name")
        );
        
        Pageable pageable = PageRequest.of(criteria.getPage(), criteria.getSize(), sort);
        
        if (criteria.getCategory() != null) {
            return productRepository.findByCategory(criteria.getCategory(), pageable);
        }
        
        return productRepository.findAll(pageable);
    }
    
    public Page<Product> searchProducts(String query, Pageable pageable) {
        // Dynamic sorting based on search relevance
        return productRepository.searchProducts(query, pageable);
    }
}

// Custom Pageable implementation
public class CustomPageRequest implements Pageable {
    
    private final int page;
    private final int size;
    private final Sort sort;
    
    public CustomPageRequest(int page, int size, Sort sort) {
        this.page = page;
        this.size = size;
        this.sort = sort;
    }
    
    @Override
    public int getPageNumber() { return page; }
    
    @Override
    public int getPageSize() { return size; }
    
    @Override
    public long getOffset() { return (long) page * size; }
    
    @Override
    public Sort getSort() { return sort; }
    
    @Override
    public Pageable next() {
        return new CustomPageRequest(page + 1, size, sort);
    }
    
    @Override
    public Pageable previousOrFirst() {
        return page == 0 ? this : new CustomPageRequest(page - 1, size, sort);
    }
    
    @Override
    public Pageable first() {
        return new CustomPageRequest(0, size, sort);
    }
    
    @Override
    public Pageable withPage(int pageNumber) {
        return new CustomPageRequest(pageNumber, size, sort);
    }
    
    @Override
    public boolean hasPrevious() { return page > 0; }
}

Custom Repository Implementations

Sometimes you need custom logic that can't be expressed with query methods:

Custom Repository Interface

// Custom interface for additional methods
public interface UserRepositoryCustom {
    List<User> findUsersWithComplexCriteria(UserSearchCriteria criteria);
    Page<User> searchUsersWithFullText(String searchTerm, Pageable pageable);
    List<UserStatistics> getUserStatistics();
    void bulkUpdateUserStatus(List<Long> userIds, boolean active);
}

// Implementation of custom methods
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<User> findUsersWithComplexCriteria(UserSearchCriteria criteria) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        if (criteria.getName() != null) {
            predicates.add(cb.like(cb.lower(root.get("name")), 
                                  "%" + criteria.getName().toLowerCase() + "%"));
        }
        
        if (criteria.getEmail() != null) {
            predicates.add(cb.like(cb.lower(root.get("email")), 
                                  "%" + criteria.getEmail().toLowerCase() + "%"));
        }
        
        if (criteria.getMinAge() != null) {
            predicates.add(cb.greaterThanOrEqualTo(root.get("age"), criteria.getMinAge()));
        }
        
        if (criteria.getMaxAge() != null) {
            predicates.add(cb.lessThanOrEqualTo(root.get("age"), criteria.getMaxAge()));
        }
        
        if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
            predicates.add(root.get("role").in(criteria.getRoles()));
        }
        
        if (criteria.getActive() != null) {
            predicates.add(cb.equal(root.get("active"), criteria.getActive()));
        }
        
        if (criteria.getCreatedAfter() != null) {
            predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), criteria.getCreatedAfter()));
        }
        
        query.where(predicates.toArray(new Predicate[0]));
        
        // Add sorting
        if (criteria.getSortBy() != null) {
            if ("desc".equalsIgnoreCase(criteria.getSortDirection())) {
                query.orderBy(cb.desc(root.get(criteria.getSortBy())));
            } else {
                query.orderBy(cb.asc(root.get(criteria.getSortBy())));
            }
        }
        
        TypedQuery<User> typedQuery = entityManager.createQuery(query);
        
        // Add pagination
        if (criteria.getLimit() != null) {
            typedQuery.setMaxResults(criteria.getLimit());
        }
        if (criteria.getOffset() != null) {
            typedQuery.setFirstResult(criteria.getOffset());
        }
        
        return typedQuery.getResultList();
    }
    
    @Override
    public Page<User> searchUsersWithFullText(String searchTerm, Pageable pageable) {
        // PostgreSQL full-text search example
        String searchQuery = "SELECT u FROM User u WHERE " +
                           "to_tsvector('english', COALESCE(u.name, '') || ' ' || COALESCE(u.email, '')) " +
                           "@@ plainto_tsquery('english', :searchTerm)";
        
        TypedQuery<User> query = entityManager.createQuery(searchQuery, User.class);
        query.setParameter("searchTerm", searchTerm);
        
        // Apply pagination
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());
        
        List<User> users = query.getResultList();
        
        // Count total results
        String countQuery = "SELECT COUNT(u) FROM User u WHERE " +
                          "to_tsvector('english', COALESCE(u.name, '') || ' ' || COALESCE(u.email, '')) " +
                          "@@ plainto_tsquery('english', :searchTerm)";
        
        TypedQuery<Long> countTypedQuery = entityManager.createQuery(countQuery, Long.class);
        countTypedQuery.setParameter("searchTerm", searchTerm);
        Long total = countTypedQuery.getSingleResult();
        
        return new PageImpl<>(users, pageable, total);
    }
    
    @Override
    public List<UserStatistics> getUserStatistics() {
        String jpql = "SELECT new com.example.dto.UserStatistics(" +
                     "u.role, COUNT(u), AVG(u.age), " +
                     "MIN(u.createdAt), MAX(u.createdAt)) " +
                     "FROM User u GROUP BY u.role";
        
        TypedQuery<UserStatistics> query = entityManager.createQuery(jpql, UserStatistics.class);
        return query.getResultList();
    }
    
    @Override
    @Transactional
    public void bulkUpdateUserStatus(List<Long> userIds, boolean active) {
        String jpql = "UPDATE User u SET u.active = :active WHERE u.id IN :ids";
        
        Query query = entityManager.createQuery(jpql);
        query.setParameter("active", active);
        query.setParameter("ids", userIds);
        
        int updatedCount = query.executeUpdate();
        System.out.println("Updated " + updatedCount + " users");
    }
}

// Extend the main repository interface
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // Standard query methods
    Optional<User> findByEmail(String email);
    List<User> findByActive(boolean active);
}

Specifications and Criteria API

For dynamic queries, Spring Data JPA supports the Criteria API through Specifications:

Using Specifications

// Enable JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
    // Standard query methods
}

// Specification utility class
public class UserSpecifications {
    
    public static Specification<User> hasName(String name) {
        return (root, query, criteriaBuilder) -> 
            name == null ? null : criteriaBuilder.like(
                criteriaBuilder.lower(root.get("name")), 
                "%" + name.toLowerCase() + "%"
            );
    }
    
    public static Specification<User> hasEmail(String email) {
        return (root, query, criteriaBuilder) -> 
            email == null ? null : criteriaBuilder.like(
                criteriaBuilder.lower(root.get("email")), 
                "%" + email.toLowerCase() + "%"
            );
    }
    
    public static Specification<User> isActive(Boolean active) {
        return (root, query, criteriaBuilder) -> 
            active == null ? null : criteriaBuilder.equal(root.get("active"), active);
    }
    
    public static Specification<User> hasRole(UserRole role) {
        return (root, query, criteriaBuilder) -> 
            role == null ? null : criteriaBuilder.equal(root.get("role"), role);
    }
    
    public static Specification<User> ageBetween(Integer minAge, Integer maxAge) {
        return (root, query, criteriaBuilder) -> {
            if (minAge == null && maxAge == null) {
                return null;
            }
            
            if (minAge != null && maxAge != null) {
                return criteriaBuilder.between(root.get("age"), minAge, maxAge);
            } else if (minAge != null) {
                return criteriaBuilder.greaterThanOrEqualTo(root.get("age"), minAge);
            } else {
                return criteriaBuilder.lessThanOrEqualTo(root.get("age"), maxAge);
            }
        };
    }
    
    public static Specification<User> createdAfter(LocalDateTime date) {
        return (root, query, criteriaBuilder) -> 
            date == null ? null : criteriaBuilder.greaterThanOrEqualTo(root.get("createdAt"), date);
    }
    
    public static Specification<User> createdBefore(LocalDateTime date) {
        return (root, query, criteriaBuilder) -> 
            date == null ? null : criteriaBuilder.lessThanOrEqualTo(root.get("createdAt"), date);
    }
}

// Service using specifications
@Service
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
        Specification<User> spec = Specification.where(null);
        
        spec = spec.and(UserSpecifications.hasName(criteria.getName()));
        spec = spec.and(UserSpecifications.hasEmail(criteria.getEmail()));
        spec = spec.and(UserSpecifications.isActive(criteria.getActive()));
        spec = spec.and(UserSpecifications.hasRole(criteria.getRole()));
        spec = spec.and(UserSpecifications.ageBetween(criteria.getMinAge(), criteria.getMaxAge()));
        spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
        spec = spec.and(UserSpecifications.createdBefore(criteria.getCreatedBefore()));
        
        return userRepository.findAll(spec, pageable);
    }
    
    public List<User> findActiveAdults() {
        Specification<User> spec = UserSpecifications.isActive(true)
                                  .and(UserSpecifications.ageBetween(18, null));
        
        return userRepository.findAll(spec);
    }
    
    public long countUsersByRole(UserRole role) {
        Specification<User> spec = UserSpecifications.hasRole(role);
        return userRepository.count(spec);
    }
}

Auditing

Spring Data JPA provides automatic auditing capabilities:

Auditing Configuration

// Enable JPA auditing
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
    
    @Bean
    public AuditorAware<String> auditorProvider() {
        return new SpringSecurityAuditorAware();
    }
}

// Auditor provider implementation
public class SpringSecurityAuditorAware implements AuditorAware<String> {
    
    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null || !authentication.isAuthenticated() || 
            authentication instanceof AnonymousAuthenticationToken) {
            return Optional.of("system");
        }
        
        return Optional.of(authentication.getName());
    }
}

// Auditable base class
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
    
    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false)
    private String createdBy;
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedBy
    @Column(name = "last_modified_by")
    private String lastModifiedBy;
    
    @LastModifiedDate
    @Column(name = "last_modified_at")
    private LocalDateTime lastModifiedAt;
    
    @Version
    private Long version;
    
    // Getters and setters
    public String getCreatedBy() { return createdBy; }
    public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    public String getLastModifiedBy() { return lastModifiedBy; }
    public void setLastModifiedBy(String lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; }
    
    public LocalDateTime getLastModifiedAt() { return lastModifiedAt; }
    public void setLastModifiedAt(LocalDateTime lastModifiedAt) { this.lastModifiedAt = lastModifiedAt; }
    
    public Long getVersion() { return version; }
    public void setVersion(Long version) { this.version = version; }
}

// Entity extending auditable base class
@Entity
@Table(name = "products")
public class Product extends AuditableEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    private String description;
    
    @Column(precision = 10, scale = 2)
    private BigDecimal price;
    
    private String category;
    
    private boolean active = true;
    
    // Constructors, getters, and setters
    public Product() {}
    
    public Product(String name, String description, BigDecimal price, String category) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.category = category;
    }
    
    // Standard 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 BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
}

Testing Repository Layers

Comprehensive testing strategies for Spring Data repositories:

Repository Testing

@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFindUserByEmail() {
        // Given
        User user = new User("John Doe", "[email protected]", 30);
        entityManager.persistAndFlush(user);
        
        // When
        Optional<User> found = userRepository.findByEmail("[email protected]");
        
        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John Doe");
    }
    
    @Test
    void shouldFindUsersByAgeBetween() {
        // Given
        entityManager.persistAndFlush(new User("Alice", "[email protected]", 25));
        entityManager.persistAndFlush(new User("Bob", "[email protected]", 35));
        entityManager.persistAndFlush(new User("Charlie", "[email protected]", 45));
        
        // When
        List<User> users = userRepository.findByAgeBetween(30, 40);
        
        // Then
        assertThat(users).hasSize(1);
        assertThat(users.get(0).getName()).isEqualTo("Bob");
    }
    
    @Test
    void shouldFindActiveUsersWithPagination() {
        // Given
        for (int i = 0; i < 25; i++) {
            User user = new User("User" + i, "user" + i + "@example.com", 20 + i);
            user.setActive(i % 2 == 0); // Even users are active
            entityManager.persistAndFlush(user);
        }
        
        // When
        Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
        Page<User> page = userRepository.findByActive(true, pageable);
        
        // Then
        assertThat(page.getContent()).hasSize(5);
        assertThat(page.getTotalElements()).isEqualTo(13); // 13 active users
        assertThat(page.getTotalPages()).isEqualTo(3);
        assertThat(page.isFirst()).isTrue();
        assertThat(page.hasNext()).isTrue();
    }
    
    @Test
    void shouldExecuteCustomQuery() {
        // Given
        User user1 = new User("John", "[email protected]", 30);
        user1.setLastLogin(LocalDateTime.now().minusDays(10));
        
        User user2 = new User("Jane", "[email protected]", 25);
        user2.setLastLogin(LocalDateTime.now().minusDays(45));
        
        entityManager.persistAndFlush(user1);
        entityManager.persistAndFlush(user2);
        
        // When
        LocalDateTime cutoff = LocalDateTime.now().minusDays(30);
        List<User> inactiveUsers = userRepository.findInactiveUsers(cutoff);
        
        // Then
        assertThat(inactiveUsers).hasSize(1);
        assertThat(inactiveUsers.get(0).getName()).isEqualTo("Jane");
    }
    
    @Test
    void shouldUpdateLastLogin() {
        // Given
        User user = new User("John", "[email protected]", 30);
        entityManager.persistAndFlush(user);
        
        LocalDateTime loginTime = LocalDateTime.now();
        
        // When
        int updated = userRepository.updateLastLogin(user.getId(), loginTime);
        entityManager.flush();
        entityManager.clear();
        
        // Then
        assertThat(updated).isEqualTo(1);
        
        User updatedUser = entityManager.find(User.class, user.getId());
        assertThat(updatedUser.getLastLogin()).isEqualToIgnoringNanos(loginTime);
    }
}

// Integration test with real database
@SpringBootTest
@Testcontainers
@Transactional
class UserRepositoryIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @Autowired
    private UserRepository userRepository;
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Test
    void shouldWorkWithRealDatabase() {
        // Given
        User user = new User("Integration Test", "[email protected]", 30);
        
        // When
        User saved = userRepository.save(user);
        Optional<User> found = userRepository.findByEmail("[email protected]");
        
        // Then
        assertThat(saved.getId()).isNotNull();
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Integration Test");
    }
}

Summary

Spring Data revolutionizes data access in Java applications:

Key Benefits:

  • Reduced Boilerplate: Automatic repository implementations
  • Query Derivation: Generate queries from method names
  • Flexible Querying: Support for JPQL, SQL, and Criteria API
  • Pagination & Sorting: Built-in pagination and sorting
  • Auditing: Automatic tracking of entity changes

Core Features:

  • Repository Interfaces: CrudRepository, PagingAndSortingRepository, JpaRepository
  • Query Methods: Derived queries from method names
  • Custom Queries: @Query annotation for complex queries
  • Specifications: Type-safe dynamic queries
  • Projections: Return only needed data

Advanced Features:

  • Custom Implementations: Extend repositories with custom logic
  • Auditing: Track who and when entities are modified
  • Transactions: Declarative transaction management
  • Caching: Integration with Spring Cache
  • Events: Entity lifecycle events

Best Practices:

  • Use appropriate repository interface based on needs
  • Leverage query derivation for simple queries
  • Use @Query for complex queries that can't be derived
  • Implement specifications for dynamic queries
  • Add auditing for entity tracking
  • Test thoroughly with @DataJpaTest

Spring Data JPA provides a powerful, flexible foundation for data access that significantly reduces development time while maintaining full control over database operations.