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.