Spring Data JPA — Complete Guide
In this tutorial, you will learn about Spring Data JPA. We cover key concepts, practical examples, and best practices to help you master this topic.
Why Spring Data JPA?
JDBC gives you fine-grained control over database access, but it requires substantial boilerplate: connection management, result set mapping, transaction handling, and SQL string construction. JPA (Jakarta Persistence) is a specification that standardizes object-relational mapping in Java, and Hibernate is its most popular implementation. Spring Data JPA builds on this by providing repository abstractions that eliminate repetitive data access code.
With Spring Data JPA, you define an interface that extends JpaRepository, and Spring automatically provides implementations for common operations like save(), findAll(), findById(), and delete(). For custom queries, you can derive queries from method names or write JPQL queries. This dramatically reduces the amount of data access code you need to write and maintain.
flowchart TB
Service[Service Layer] --> Repository[JpaRepository Interface]
Repository --> SIP[SimpleJpaRepository
Spring Implementation]
SIP --> EM[EntityManager]
EM --> ORM[Hibernate ORM]
ORM --> JDBC[JDBC]
JDBC --> DB[(Database)]
Service -.-> QM[Query Methods
findByName]
QM --> SIP
Service -.-> JPQL[@Query JPQL]
JPQL --> SIP
Service -.-> Criteria[Criteria API]
Criteria --> EM
Defining Entities
An entity is a Java class mapped to a database table.
package com.example.blog.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.*;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Column(nullable = false, unique = true)
private String slug;
@Enumerated(EnumType.STRING)
private PostStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
@ManyToMany
@JoinTable(
name = "post_tags",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
@Column(updatable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// getters and setters
}
enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED
}
Relationship Mapping
| Annotation | Database Equivalent | Fetch Type Default |
|---|---|---|
@OneToOne |
Foreign key column | EAGER |
@ManyToOne |
Foreign key column | EAGER |
@OneToMany |
Join column on the many side | LAZY |
@ManyToMany |
Join table | LAZY |
Creating Repositories
package com.example.blog.repository;
import com.example.blog.model.*;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
// Derived query methods
List<Post> findByStatus(PostStatus status);
List<Post> findByTitleContainingIgnoreCase(String keyword);
Optional<Post> findBySlug(String slug);
long countByStatus(PostStatus status);
boolean existsBySlug(String slug);
// JPQL query
@Query("SELECT p FROM Post p LEFT JOIN FETCH p.author WHERE p.status = :status")
List<Post> findAllWithAuthorByStatus(@Param("status") PostStatus status);
// Native SQL query
@Query(value = "SELECT * FROM posts WHERE extract(month from created_at) = :month",
nativeQuery = true)
List<Post> findByCreatedMonth(@Param("month") int month);
// Modifying query
@Modifying
@Query("UPDATE Post p SET p.status = :status WHERE p.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") PostStatus status);
}
Using the Repository
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
@Transactional(readOnly = true)
public List<Post> getPublishedPosts() {
return postRepository.findAllWithAuthorByStatus(PostStatus.PUBLISHED);
}
@Transactional
public Post createPost(CreatePostRequest request, Author author) {
Post post = new Post();
post.setTitle(request.title());
post.setContent(request.content());
post.setSlug(generateSlug(request.title()));
post.setStatus(PostStatus.DRAFT);
post.setAuthor(author);
return postRepository.save(post);
}
@Transactional
public void deletePost(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
postRepository.delete(post);
}
}
Paging and Sorting
Spring Data JPA provides built-in paging support.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
public Page<Post> listPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort).descending());
return postRepository.findAll(pageable);
}
}
{
"content": [ ... ],
"pageable": { "pageNumber": 0, "pageSize": 20 },
"totalElements": 156,
"totalPages": 8,
"last": false,
"first": true
}
Specifications for Dynamic Queries
For complex search scenarios, use JPA Specifications.
public class PostSpecifications {
public static Specification<Post> hasStatus(PostStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<Post> titleContains(String keyword) {
return (root, query, cb) -> {
if (keyword == null || keyword.isEmpty()) return null;
return cb.like(cb.lower(root.get("title")),
"%" + keyword.toLowerCase() + "%");
};
}
public static Specification<Post> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
}
@Service
public class SearchService {
private final PostRepository postRepository;
public List<Post> searchPosts(String keyword, PostStatus status,
LocalDateTime after, Pageable pageable) {
Specification<Post> spec = Specification
.where(PostSpecifications.titleContains(keyword))
.and(PostSpecifications.hasStatus(status))
.and(PostSpecifications.createdAfter(after));
return postRepository.findAll(spec, pageable).getContent();
}
}
Transactions and Locking
@Service
public class CommentService {
@Transactional
public Comment addComment(Long postId, String content, String author) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new PostNotFoundException(postId));
Comment comment = new Comment();
comment.setContent(content);
comment.setAuthor(author);
comment.setPost(post);
post.getComments().add(comment); // Cascade persist
return commentRepository.save(comment);
}
// Pessimistic locking
@Transactional
@Lock(LockModeType.PESSIMISTIC_WRITE)
public Post getPostForUpdate(Long id) {
return postRepository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
}
}
Common Mistakes
1. LazyInitializationException
Accessing a lazy-loaded relationship outside of a transaction throws LazyInitializationException.
// Fails: session is closed, trying to access lazy comments
@GetMapping("/posts/{id}")
public Post getPost(@PathVariable Long id) {
Post post = postRepository.findById(id).orElseThrow();
post.getComments().size(); // LazyInitializationException!
}
// Fix 1: Eager fetch (but beware of performance)
// Fix 2: @EntityGraph
@EntityGraph(attributePaths = {"comments", "author"})
Optional<Post> findById(Long id);
// Fix 3: JPQL FETCH JOIN
@Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments WHERE p.id = :id")
Optional<Post> findByIdWithComments(@Param("id") Long id);
2. N+1 Query Problem
// N+1: one query for posts, then N queries for comments
List<Post> posts = postRepository.findAll();
for (Post post : posts) {
System.out.println(post.getComments().size()); // triggers query each time
}
// Fix: use JOIN FETCH or @EntityGraph
@Query("SELECT DISTINCT p FROM Post p LEFT JOIN FETCH p.comments")
List<Post> findAllWithComments();
3. Opening Entity Manager in View (OSIV)
Spring Boot enables OSIV by default, keeping the EntityManager open during view rendering. This can cause Connection Pool exhaustion in high-traffic applications.
spring:
jpa:
open-in-view: false
4. Forgetting @Transactional for Write Operations
Without @Transactional, each JPA operation runs in its own transaction, and cascading operations may fail silently.
5. Using CascadeType.ALL Without Thought
CascadeType.ALL on @ManyToMany can cause unintended deletions. Be specific about which cascade types you need.
6. Ignoring Database Schema Changes
JPA's ddl-auto: update is convenient for development but dangerous in production. Use validate or Flyway/Liquibase for production schema management.
Practice Questions
- What is the difference between
@ManyToOneand@OneToMany? Which side owns the relationship? - How does Spring Data JPA derive queries from method names?
- What is the N+1 Problem and how do you solve it?
- Why should
open-in-viewbe disabled in production? - What is the difference between JPQL and native queries?
Challenge: Build a query DSL that allows users to filter posts by multiple criteria (status, tags, date range, title search) using JPA Specifications. Support sorting by any field and pagination. Return the results as a Page with proper metadata.
FAQ
Mini Project: E-Commerce Order System
Build a Spring Data JPA-backed order management system with the following entities:
Customerwith addresses (OneToMany)Productwith categories (ManyToMany) and inventory trackingOrderwith line items (OneToMany) and status lifecyclePaymentwith type, amount, and payment date
Implement:
- Repository layer with custom queries for order history, product search, and customer spending
- Pagination and sorting for product listings
- Pessimistic locking for inventory deduction during order placement
- Transactional rollback if payment processing fails
- Specifications for combined search/filter queries
What's Next
With Spring Data JPA mastered, you are ready to build complete REST APIs. In the next lesson, you will learn Building REST APIs with Spring, including validation, error handling, and documentation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro