Skip to content

Spring Data JPA — Complete Guide

DodaTech Updated 2026-06-28 7 min read

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

  1. What is the difference between @ManyToOne and @OneToMany? Which side owns the relationship?
  2. How does Spring Data JPA derive queries from method names?
  3. What is the N+1 Problem and how do you solve it?
  4. Why should open-in-view be disabled in production?
  5. 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

What is the difference between JPA and Hibernate?

JPA is a specification (interface) for ORM in Java. Hibernate is an implementation of that specification. Spring Data JPA works with any JPA implementation, but Hibernate is the default.

Should I use derived query methods or @Query for custom queries?

Use derived methods for simple queries based on field values. Use @Query for complex queries, joins, aggregations, or when you need to optimize the generated SQL.

What is the difference between merge and persist?

persist() attaches a new entity to the persistence context. merge() copies state from a detached entity to a managed entity, creating it if necessary. In Spring Data, save() handles both cases.

How do I map inheritance hierarchies?

JPA supports three inheritance strategies: SINGLE_TABLE (all classes in one table), JOINED (each class in its own table with joins), and TABLE_PER_CLASS (each class in its own table with all columns).

Can Spring Data JPA work with NoSQL databases?

Yes. Spring Data provides similar repository abstractions for MongoDB, Cassandra, Redis, Neo4j, and others. The query derivation pattern works across all supported databases.

Mini Project: E-Commerce Order System

Build a Spring Data JPA-backed order management system with the following entities:

  • Customer with addresses (OneToMany)
  • Product with categories (ManyToMany) and inventory tracking
  • Order with line items (OneToMany) and status lifecycle
  • Payment with 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