Master Spring Data JPA: 131 Real‑World Spring Boot 3 Cases & Best Practices
This article presents a comprehensive guide to Spring Data JPA in Spring Boot 3, covering 131 practical cases, core repository interfaces, Specification and Criteria Builder techniques, and dozens of performance‑optimizing tips such as lazy loading, caching, batch processing, transaction management, projections, and stream queries.
1. Introduction
Spring Data JPA is a powerful API for data handling in Spring Boot projects. It greatly improves development efficiency for complex data management tasks.
Common default annotations include:
@Repository : marks a DAO class (often optional).
@Query : allows native queries.
As data volume grows, developers face challenges like slow queries, complex relationships, and query optimization. The following sections dive deep into Spring Data JPA practical techniques.
2. Practical Cases
2.1 Basic Concepts
When defining repository interfaces, you can extend one of the following:
Repository : the most basic interface with no predefined methods.
<code>public interface UserRepository extends Repository<User, Long> {
// no predefined methods
}</code>It is generally not recommended to use this interface directly.
CrudRepository : provides basic CRUD operations.
<code>public interface UserRepository extends CrudRepository<User, Long> {
// basic CRUD methods
}</code>Key methods:
save(S entity)
findById(ID id)
existsById(ID id)
findAll()
deleteById(ID id)
PagingAndSortingRepository : adds pagination and sorting methods on top of CRUD.
<code>public interface UserRepository extends PagingAndSortingRepository<User, Long> {
// CRUD + pagination + sorting
}</code>findAll(Pageable pageable)
findAll(Sort sort)
JpaRepository : the most feature‑rich interface, adding JPA‑specific methods, batch operations, custom queries, and flush control.
<code>public interface UserRepository extends JpaRepository<User, Long> {
// full CRUD, pagination, sorting, JPA‑specific methods
}</code>Custom query methods can be defined by following the findBy naming convention, e.g.:
<code>public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById(Long id);
List<User> findByName(String name);
List<User> findByAgeGreaterThan(int age);
}</code>2.2 Using Specification and Criteria Builder
For complex queries that cannot be expressed with simple method names, Specification and Criteria Builder enable dynamic query construction.
Specification is a functional interface that builds predicates programmatically.
<code>import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class UserSpecification {
public static Specification<MyEntity> hasName(String name) {
return (Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
return cb.equal(root.get("name"), name);
};
}
}</code>Usage example:
<code>private final UserRepository userRepository;
Specification<User> spec = UserSpecification.hasName("Pack");
List<User> results = userRepository.findAll(spec);
</code>Criteria Builder is part of JPA and allows type‑safe query creation.
<code>@Service
public class UserService {
@PersistenceContext
private EntityManager em;
public List<User> findByName(String name) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.select(root).where(cb.equal(root.get("name"), name));
return em.createQuery(query).getResultList();
}
}
</code>Multiple specifications can be combined:
<code>Specification<User> spec = Specification.where(UserSpecification.hasName("Pack"))
.and(UserSpecification.hasStatus(1))
.and(UserSpecification.hasAgeGreaterThan(25));
List<User> results = userRepository.findAll(spec);
</code>2.3 Development Tips
Lazy Loading : Set relationships to FetchType.LAZY to avoid unnecessary data fetching, but be aware of N+1 problems.
<code>@Entity
public class Customer {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "customer")
private List<Order> orders;
}
</code>Query Optimization : Prefer a single well‑crafted query over multiple queries; use JPQL, Criteria API, or native SQL when needed.
<code>@Query("SELECT e FROM User e JOIN FETCH e.orders WHERE e.name = :name")
List<MyEntity> findByNameWithOrders(@Param("name") String name);
</code>Caching : Cache frequently accessed, rarely changed data using Spring’s cache abstraction.
<code>@Cacheable("users")
public List<User> findAll() {
return userRepository.findAll();
}
</code>Batch Processing : Use saveAll and deleteInBatch for bulk operations.
<code>public void saveUsers(List<User> users) {
userRepository.saveAll(users);
}
public void deleteUsers(List<User> users) {
userRepository.deleteInBatch(users);
}
</code>Transaction Management : Wrap database operations in transactions using @Transactional .
<code>@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void updateUsers(List<User> users) {
for (User user : users) {
userRepository.save(user);
}
}
}
</code>Avoid N+1 Problem : Use JOIN FETCH in JPQL to retrieve related entities in a single query.
<code>@Query("SELECT e FROM Customer e JOIN FETCH e.orders WHERE e.status = :status")
List<Customer> findByStatusWithOrders(@Param("status") Integer status);
</code>Projections : Retrieve only required fields to reduce data transfer.
<code>public interface UserProjection {
String getName();
String getStatus();
}
@Query("SELECT e.name AS name, e.status AS status FROM User e WHERE e.age > :age")
List<UserProjection> queryUser(@Param("age") int age);
</code>Entity Auditing : Use @CreatedDate , @LastModifiedDate , @CreatedBy , and @LastModifiedBy for automatic audit fields.
<code>@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
</code>Locking : Apply pessimistic or optimistic locks with @Lock and @Transactional .
<code>public interface ProductRepository extends JpaRepository<Product, Long> {
@Transactional
@Lock(LockModeType.PESSIMISTIC_READ)
Product findByName(String name);
}
</code>Stream Queries : Return Stream<T> for incremental processing, remembering to close the stream.
<code>@Query("select u from User u")
Stream<User> findUserByStream();
try (Stream<User> stream = repository.findUserByStream()) {
stream.forEach(...);
}
</code>Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.