java5 min read

Spring Boot JPA Tutorial: Learn Data Access from Scratch (2026)

Spring Boot JPA Tutorial: Learn Data Access from Scratch (2026)

Published:  |  Category: Java  |  Reading time: ~15 min
Spring Boot JPA Tutorial: Learn Data Access from Scratch (2026)

Spring Boot JPA combines the power of Spring Boot's auto-configuration with the Jakarta Persistence API for effortless data access. Adding spring-boot-starter-data-jpa to your project gives you a fully configured JPA setup with Hibernate, HikariCP connection pool, and Spring Data repositories. I have used Spring Boot JPA across projects of every scale, from simple CRUD services to complex domain models with dozens of entity relationships.

This tutorial covers configuration, repository patterns, query methods, auditing, specification-based queries, and performance tuning specific to Spring Boot's JPA integration.

Auto-Configuration and Properties

Spring Boot auto-configures a DataSource (from properties), an EntityManagerFactory, a JpaTransactionManager, and a PlatformTransactionManager. You control behavior via spring.jpa.* and spring.datasource.* properties. Spring Boot's Hibernate dialect auto-detection works for major databases.

For development, use spring.jpa.show-sql=true to log generated SQL. Never enable these in production — they expose schema details and impact performance. Use a dedicated logging category instead.

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: app_user
    password: ${DATABASE_PASSWORD}
    hikari:
      maximum-pool-size: 15
      minimum-idle: 5
      connection-timeout: 3000
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
    open-in-view: false

Spring Data Repositories

Spring Data repositories eliminate boilerplate data-access code. Extend JpaRepository for CRUD operations, PagingAndSortingRepository for pagination. Spring Data generates implementations at runtime based on method names: findByEmailIgnoreCase, countByStatusAndCreatedAtBetween.

Custom queries use @Query with JPQL or native SQL. Use projections (interfaces with getters) to fetch only specific columns. For dynamic queries, combine Specification with JpaSpecificationExecutor.

public interface OrderRepository extends JpaRepository,
        JpaSpecificationExecutor {
    
    List findByCustomerIdAndStatus(Long customerId, OrderStatus status);
    
    @Query("SELECT o FROM Order o WHERE o.total > :min AND o.createdAt > :since")
    List findRecentLargeOrders(@Param("min") BigDecimal min,
                                      @Param("since") LocalDateTime since);
    
    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int bulkUpdateStatus(@Param("ids") List ids, @Param("status") OrderStatus status);
    
    // Projection
    interface OrderSummary {
        Long getId();
        BigDecimal getTotal();
        LocalDateTime getCreatedAt();
    }
    List findByCustomerId(Long customerId);
}

Auditing with Spring Data

Spring Data JPA provides auditing annotations to automatically populate created and last-modified fields. Enable auditing with @EnableJpaAuditing on a configuration class. Annotate entity fields with @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy.

Implement AuditorAware to supply the current user (from Spring Security or a custom context). This eliminates repetitive timestamp-setting code in every service method and ensures consistency across the entire application.

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
    @Bean
    public AuditorAware auditorAware() {
        return () -> Optional.ofNullable(
            SecurityContextHolder.getContext().getAuthentication()
        ).map(auth -> auth.getName());
    }
}

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
}

Specifications for Dynamic Queries

When queries have optional filters — common in search and reporting screens — specifications provide a type-safe alternative to concatenating JPQL strings. Implement the Specification interface, which builds Predicate instances from a root query. Combine specifications with and(), or(), and where().

Create static factory methods for each filter criterion. Callers compose specifications based on which filters the user selected. Spring Data executes the resulting predicate against the database, generating optimized SQL.

public class OrderSpecifications {
    public static Specification hasCustomerId(Long customerId) {
        return (root, query, cb) -> 
            customerId == null ? null : cb.equal(root.get("customerId"), customerId);
    }
    
    public static Specification hasStatus(OrderStatus status) {
        return (root, query, cb) -> 
            status == null ? null : cb.equal(root.get("status"), status);
    }
    
    public static Specification createdAfter(LocalDateTime date) {
        return (root, query, cb) -> 
            date == null ? null : cb.greaterThanOrEqualTo(root.get("createdAt"), date);
    }
}

// Usage
Specification spec = Specification
    .where(hasStatus(request.status()))
    .and(createdAfter(request.startDate()));
Page results = repository.findAll(spec, pageable);

Performance Optimization Tips

Spring Boot JPA performance hinges on understanding what SQL Hibernate generates. Enable batch inserts/updates with hibernate.jdbc.batch_size and ordered inserts/updates. Use @BatchSize on collections to batch lazy loads.

Disable open-in-view (spring.jpa.open-in-view=false) — the default true keeps a database connection open through the entire HTTP request, causing connection exhaustion. Use entity graphs or DTO projections for read-heavy endpoints instead of loading full entities.

# Performance tuning
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 30
          batch_versioned_data: true
        order_inserts: true
        order_updates: true
        default_batch_fetch_size: 20
    open-in-view: false

// DTO projection query
@Query("""
    SELECT new com.example.dto.OrderSummaryDTO(
        o.id, o.customerId, o.total, o.createdAt
    ) FROM Order o WHERE o.customerId = :customerId
""")
List findSummariesByCustomerId(@Param("customerId") Long customerId);

Pagination and Sorting

Spring Data JPA provides Pageable and Page abstractions for paginated queries. Accept a Pageable parameter in repository methods and return Page for results with total count, page number, and sort info. The web integration automatically binds page, size, and sort query parameters from HTTP requests.

Use Slice instead of Page when you only need next-page availability without total count — it avoids the COUNT query entirely, improving performance on large datasets. For custom sorting, use Sort.by() with property references.

@GetMapping("/api/orders")
public Page getOrders(
        @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
    return orderRepository.findAll(pageable)
        .map(OrderResponse::from);
}

// Custom paginated query
@Query("SELECT o FROM Order o WHERE o.customerId = :customerId")
Slice findByCustomerId(@Param("customerId") Long customerId, Pageable pageable);

// Usage in service
Pageable pageable = PageRequest.of(0, 10, Sort.by("total").descending());
Page page = repository.findByCustomerId(42L, pageable);
// page.getContent(), page.getTotalPages(), page.hasNext()

Frequently Asked Questions

What is the difference between Spring Data JPA and Spring Data JDBC?

Spring Data JPA uses JPA (typically Hibernate) with lazy loading, caching, and dirty checking. Spring Data JDBC is a simpler, direct mapping approach without lazy loading or session management.

How do I handle soft deletes with Spring Data JPA?

Use @SQLRestriction("deleted = false") on the entity to filter out deleted rows. Add a boolean deleted field. Override the repository's delete method to set deleted=true instead of removing the row.

What is the N+1 problem in Spring Data JPA?

N+1 occurs when iterating over a collection triggers a separate query for each element's lazily-loaded association. Fix with @EntityGraph on repository methods, JOIN FETCH in @Query, or @BatchSize.

Should I use Hibernate DDL auto-generation in production?

No. Use ddl-auto: validate in production — it checks entities match the schema and fails on startup. Use Flyway or Liquibase for schema migrations. Use update or create-drop only in development.

Originally published on Ayodhyyya. Last updated June 1, 2026.