StackWise
Spring Boot 4Data Access

Spring Data JPA Repositories

Entities, repository interfaces, derived queries, @Query, projections, pagination, auditing and optimistic locking with Hibernate 7.

beginner 3 min readLesson 11 of 23

The layers

flowchart LR
  S[OrderService] --> R["OrderRepository<br/>(your interface)"]
  R --> SD["Spring Data<br/>generated proxy"]
  SD --> EM["JPA EntityManager<br/>(Hibernate 7)"]
  EM --> H[(HikariCP pool)]
  H --> DB[(PostgreSQL)]
  • JPA (Jakarta Persistence 3.2) is the specification for object-relational mapping.
  • Hibernate is the implementation Spring Boot uses.
  • Spring Data JPA generates repository implementations from interfaces, removing DAO boilerplate.

An entity

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Column(nullable = false, length = 40)
    private String customerId;

    @Enumerated(EnumType.STRING)                   // never ORDINAL — reordering the enum corrupts data
    @Column(nullable = false)
    private OrderStatus status = OrderStatus.NEW;

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal total = BigDecimal.ZERO;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLine> lines = new ArrayList<>();

    @Version                                       // optimistic locking
    private long version;

    @CreatedDate private Instant createdAt;
    @LastModifiedDate private Instant updatedAt;

    protected Order() {}                           // required by JPA
    public Order(String customerId) { this.customerId = customerId; }

    public void addLine(String sku, int qty, BigDecimal price) {   // keep both sides in sync
        var line = new OrderLine(this, sku, qty, price);
        lines.add(line);
        total = total.add(price.multiply(BigDecimal.valueOf(qty)));
    }
    // getters…
}

A repository

public interface OrderRepository extends JpaRepository<Order, Long> {

    // Derived queries — parsed from the method name
    List<Order> findByCustomerIdAndStatus(String customerId, OrderStatus status);
    Optional<Order> findFirstByCustomerIdOrderByCreatedAtDesc(String customerId);
    boolean existsByCustomerIdAndStatus(String customerId, OrderStatus status);
    long countByStatus(OrderStatus status);
    Page<Order> findByStatus(OrderStatus status, Pageable pageable);

    // JPQL for anything non-trivial
    @Query("""
           select o from Order o
           where o.status = :status and o.total >= :min
           order by o.total desc
           """)
    List<Order> findLargeOrders(OrderStatus status, BigDecimal min, Limit limit);

    // Native SQL when you need database features
    @Query(value = "select * from orders where created_at > now() - interval '1 day'", nativeQuery = true)
    List<Order> placedInLastDay();

    // Bulk update — bypasses the persistence context
    @Modifying(clearAutomatically = true)
    @Query("update Order o set o.status = 'EXPIRED' where o.status = 'NEW' and o.createdAt < :cutoff")
    int expireOlderThan(Instant cutoff);
}

JpaRepository already provides save, saveAll, findById, findAll(Sort/Pageable), deleteById, count and more.

Projections — fetch only what you need

// Interface-based or record-based projections select only those columns
public record OrderSummary(Long id, OrderStatus status, BigDecimal total) {}

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<OrderSummary> findByCustomerId(String customerId);

    @Query("select new com.shop.orders.OrderSummary(o.id, o.status, o.total) from Order o where o.status = :s")
    Page<OrderSummary> summaries(OrderStatus s, Pageable pageable);
}

Projections avoid loading full entities (and their associations) for read-only screens — often the single biggest JPA performance win.

Pagination and sorting

Pageable page = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<Order> result = repository.findByStatus(OrderStatus.PAID, page);
result.getContent(); result.getTotalElements(); result.getTotalPages();

Page runs an extra count query; return Slice<T> when you only need “is there a next page?”. For deep pagination over large tables, prefer keyset (seek) pagination with ScrollPosition / Window<T>.

Auditing

@Configuration
@EnableJpaAuditing
class JpaConfig {
    @Bean AuditorAware<String> auditor() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).map(Authentication::getName);
    }
}

Add @EntityListeners(AuditingEntityListener.class) to entities (or a mapped superclass) and @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy are filled in automatically.

Optimistic locking

With a @Version column, every UPDATE includes where id = ? and version = ?. If another transaction changed the row first, zero rows match and Spring throws ObjectOptimisticLockingFailureException — map it to 409 Conflict or retry. This prevents lost updates without holding database locks.

Essential configuration

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/shop
    hikari:
      maximum-pool-size: 20
  jpa:
    open-in-view: false            # see the callout
    hibernate:
      ddl-auto: validate           # schema is owned by Flyway/Liquibase, never 'update' in prod
    properties:
      hibernate.jdbc.batch_size: 50
      hibernate.order_inserts: true
Finished this lesson?

Mark it complete to track your progress through Spring Boot 4.