Skip to content

Spring Data JPA

Overview

Spring Data JPA simplifies data access by reducing boilerplate code through repository abstractions.

Spring Data JPA Architecture


Entity Mapping

Basic Entity

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

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

    @Column(name = "order_number", unique = true, nullable = false, length = 50)
    private String orderNumber;

    @Column(precision = 10, scale = 2)
    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private OrderStatus status;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @Version
    private Long version;  // Optimistic locking

    // Getters and setters
}

public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

ID Generation Strategies

// Auto (provider chooses)
@GeneratedValue(strategy = GenerationType.AUTO)

// Identity (auto-increment column)
@GeneratedValue(strategy = GenerationType.IDENTITY)

// Sequence (database sequence)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
@SequenceGenerator(name = "order_seq", sequenceName = "ORDER_SEQ", allocationSize = 50)

// Table (simulated sequence)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "order_gen")
@TableGenerator(name = "order_gen", table = "id_generator",
    pkColumnName = "gen_name", valueColumnName = "gen_value", allocationSize = 50)

// UUID
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Relationships

One-to-Many / Many-to-One

@Entity
public class Order {

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

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    // Helper method to maintain bidirectional relationship
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
    }
}

@Entity
public class OrderItem {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    private String productId;
    private int quantity;
    private BigDecimal price;
}

Many-to-Many

@Entity
public class Product {

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

    @ManyToMany
    @JoinTable(
        name = "product_categories",
        joinColumns = @JoinColumn(name = "product_id"),
        inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private Set<Category> categories = new HashSet<>();
}

@Entity
public class Category {

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

    @ManyToMany(mappedBy = "categories")
    private Set<Product> products = new HashSet<>();
}

One-to-One

@Entity
public class User {

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

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
              fetch = FetchType.LAZY, optional = false)
    private UserProfile profile;
}

@Entity
public class UserProfile {

    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId  // Share primary key with User
    @JoinColumn(name = "user_id")
    private User user;
}

Repository Interface

Repository Hierarchy

Repository Hierarchy

Basic Repository

public interface OrderRepository extends JpaRepository<Order, Long> {

    // Query derivation from method name
    List<Order> findByStatus(OrderStatus status);

    List<Order> findByCustomerId(Long customerId);

    List<Order> findByStatusAndCreatedAtAfter(OrderStatus status, LocalDateTime after);

    Optional<Order> findByOrderNumber(String orderNumber);

    boolean existsByOrderNumber(String orderNumber);

    long countByStatus(OrderStatus status);

    void deleteByStatus(OrderStatus status);

    // Pagination and sorting
    Page<Order> findByCustomerId(Long customerId, Pageable pageable);

    Slice<Order> findByStatus(OrderStatus status, Pageable pageable);

    List<Order> findByCustomerIdOrderByCreatedAtDesc(Long customerId);
}

Query Method Keywords

Query Method Keywords


Custom Queries

@Query Annotation

public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL query
    @Query("SELECT o FROM Order o WHERE o.customer.email = :email")
    List<Order> findByCustomerEmail(@Param("email") String email);

    // Native SQL query
    @Query(value = "SELECT * FROM orders WHERE total_amount > :amount",
           nativeQuery = true)
    List<Order> findHighValueOrders(@Param("amount") BigDecimal amount);

    // With pagination
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    Page<Order> findByStatusWithPaging(@Param("status") OrderStatus status,
                                       Pageable pageable);

    // Projection
    @Query("SELECT o.orderNumber, o.totalAmount FROM Order o WHERE o.customer.id = :customerId")
    List<Object[]> findOrderSummary(@Param("customerId") Long customerId);

    // Update query
    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);

    // Delete query
    @Modifying
    @Query("DELETE FROM Order o WHERE o.status = :status AND o.createdAt < :before")
    int deleteOldOrders(@Param("status") OrderStatus status,
                        @Param("before") LocalDateTime before);

    // Join fetch (prevent N+1)
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :customerId")
    List<Order> findByCustomerIdWithItems(@Param("customerId") Long customerId);
}

Named Queries

@Entity
@NamedQueries({
    @NamedQuery(
        name = "Order.findByStatusAndDateRange",
        query = "SELECT o FROM Order o WHERE o.status = :status " +
                "AND o.createdAt BETWEEN :start AND :end"
    ),
    @NamedQuery(
        name = "Order.countByCustomer",
        query = "SELECT COUNT(o) FROM Order o WHERE o.customer.id = :customerId"
    )
})
public class Order {
    // ...
}

// Repository uses named query by convention: Entity.methodName
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatusAndDateRange(OrderStatus status,
                                         LocalDateTime start,
                                         LocalDateTime end);
}

Projections

Interface Projection

// Closed projection (specific fields)
public interface OrderSummary {
    String getOrderNumber();
    BigDecimal getTotalAmount();
    OrderStatus getStatus();

    // Nested projection
    CustomerInfo getCustomer();

    interface CustomerInfo {
        String getName();
        String getEmail();
    }
}

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<OrderSummary> findByStatus(OrderStatus status);

    @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
    List<OrderSummary> findSummaryByCustomerId(@Param("customerId") Long customerId);
}

Class-Based Projection (DTO)

public record OrderDTO(
    Long id,
    String orderNumber,
    BigDecimal totalAmount,
    String customerName
) {}

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT new com.example.dto.OrderDTO(o.id, o.orderNumber, " +
           "o.totalAmount, o.customer.name) FROM Order o WHERE o.status = :status")
    List<OrderDTO> findDTOByStatus(@Param("status") OrderStatus status);
}

Dynamic Projection

public interface OrderRepository extends JpaRepository<Order, Long> {

    <T> List<T> findByStatus(OrderStatus status, Class<T> type);

    <T> Optional<T> findById(Long id, Class<T> type);
}

// Usage
List<OrderSummary> summaries = orderRepository.findByStatus(PENDING, OrderSummary.class);
List<OrderDTO> dtos = orderRepository.findByStatus(PENDING, OrderDTO.class);

Pagination and Sorting

Pageable

public interface OrderRepository extends JpaRepository<Order, Long> {
    Page<Order> findByStatus(OrderStatus status, Pageable pageable);
    Slice<Order> findByCustomerId(Long customerId, Pageable pageable);
}

// Usage
Pageable pageable = PageRequest.of(
    0,                              // Page number (0-indexed)
    20,                             // Page size
    Sort.by("createdAt").descending()
);

Page<Order> page = orderRepository.findByStatus(OrderStatus.PENDING, pageable);

// Page info
page.getContent();        // List<Order>
page.getTotalElements();  // Total count
page.getTotalPages();     // Total pages
page.getNumber();         // Current page
page.getSize();           // Page size
page.hasNext();           // Has more pages
page.hasPrevious();       // Has previous page

Sorting

// Simple sort
Sort sort = Sort.by("createdAt").descending();

// Multiple fields
Sort sort = Sort.by(
    Sort.Order.desc("status"),
    Sort.Order.asc("createdAt")
);

// Type-safe sort with JpaSort
Sort sort = JpaSort.unsafe("LENGTH(orderNumber)");

// In repository method
List<Order> findByCustomerId(Long customerId, Sort sort);

// Usage
orderRepository.findByCustomerId(123L, Sort.by("totalAmount").descending());

Slice vs Page

Slice vs Page


Specifications (Dynamic Queries)

@Repository
public interface OrderRepository extends JpaRepository<Order, Long>,
        JpaSpecificationExecutor<Order> {
}

// Specification class
public class OrderSpecifications {

    public static Specification<Order> hasStatus(OrderStatus status) {
        return (root, query, cb) ->
            status == null ? null : cb.equal(root.get("status"), status);
    }

    public static Specification<Order> createdAfter(LocalDateTime date) {
        return (root, query, cb) ->
            date == null ? null : cb.greaterThan(root.get("createdAt"), date);
    }

    public static Specification<Order> totalAmountGreaterThan(BigDecimal amount) {
        return (root, query, cb) ->
            amount == null ? null : cb.greaterThan(root.get("totalAmount"), amount);
    }

    public static Specification<Order> customerNameLike(String name) {
        return (root, query, cb) -> {
            if (name == null) return null;
            Join<Order, Customer> customer = root.join("customer");
            return cb.like(cb.lower(customer.get("name")),
                          "%" + name.toLowerCase() + "%");
        };
    }
}

// Usage
Specification<Order> spec = Specification
    .where(OrderSpecifications.hasStatus(OrderStatus.PENDING))
    .and(OrderSpecifications.createdAfter(LocalDateTime.now().minusDays(7)))
    .and(OrderSpecifications.totalAmountGreaterThan(new BigDecimal("100")));

List<Order> orders = orderRepository.findAll(spec);
Page<Order> pagedOrders = orderRepository.findAll(spec, pageable);
long count = orderRepository.count(spec);

Auditing

Enable Auditing

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
}

Auditable Entity

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;
}

@Entity
public class Order extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
}

Transaction Management

@Transactional

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    @Transactional  // Read-write transaction
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order();
        // ... set fields

        inventoryService.reserve(order.getItems());  // Same transaction
        paymentService.authorize(order.getPayment()); // Same transaction

        return orderRepository.save(order);
    }

    @Transactional(readOnly = true)  // Optimized for reads
    public Order getOrder(Long id) {
        return orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditOrder(Order order) {
        // Runs in separate transaction
    }

    @Transactional(timeout = 30)  // Timeout in seconds
    public void processLargeOrder(Order order) {
        // Long-running operation
    }

    @Transactional(rollbackFor = BusinessException.class)
    public void businessOperation() throws BusinessException {
        // Rolls back for BusinessException (checked exception)
    }

    @Transactional(noRollbackFor = WarningException.class)
    public void operationWithWarning() throws WarningException {
        // Does NOT rollback for WarningException
    }
}

Propagation Types

Transaction Propagation Types


Entity Lifecycle Callbacks

@Entity
@EntityListeners(OrderEntityListener.class)
public class Order {

    @PrePersist
    public void prePersist() {
        if (orderNumber == null) {
            orderNumber = generateOrderNumber();
        }
    }

    @PostPersist
    public void postPersist() {
        // After entity is persisted
    }

    @PreUpdate
    public void preUpdate() {
        // Before entity is updated
    }

    @PostUpdate
    public void postUpdate() {
        // After entity is updated
    }

    @PreRemove
    public void preRemove() {
        // Before entity is deleted
    }

    @PostRemove
    public void postRemove() {
        // After entity is deleted
    }

    @PostLoad
    public void postLoad() {
        // After entity is loaded
    }
}

// External listener
public class OrderEntityListener {

    @PostPersist
    public void afterPersist(Order order) {
        // Can inject beans via ObjectFactory
    }
}

N+1 Problem and Solutions

The Problem

// This causes N+1 queries
List<Order> orders = orderRepository.findAll();  // 1 query
for (Order order : orders) {
    order.getItems().size();  // N queries (one per order)
}

Solutions

// 1. JOIN FETCH in query
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);

// 2. @EntityGraph
@EntityGraph(attributePaths = {"items", "customer"})
List<Order> findByStatus(OrderStatus status);

// Named entity graph
@Entity
@NamedEntityGraph(
    name = "Order.withItemsAndCustomer",
    attributeNodes = {
        @NamedAttributeNode("items"),
        @NamedAttributeNode("customer")
    }
)
public class Order { }

@EntityGraph(value = "Order.withItemsAndCustomer")
List<Order> findByStatus(OrderStatus status);

// 3. Batch fetching (in entity)
@OneToMany(mappedBy = "order")
@BatchSize(size = 50)
private List<OrderItem> items;

// 4. Subselect fetching
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;

Performance Tips

// 1. Use projections for read-only data
List<OrderSummary> summaries = orderRepository.findProjectedByStatus(status);

// 2. Use readOnly transactions
@Transactional(readOnly = true)
public List<Order> getOrders() { }

// 3. Batch inserts/updates
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

// 4. Use saveAll for bulk
orderRepository.saveAll(orders);  // Uses batching

// 5. Avoid fetching entities for updates
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
int updateStatusBatch(@Param("ids") List<Long> ids, @Param("status") OrderStatus status);

// 6. Use pagination
Page<Order> orders = orderRepository.findAll(PageRequest.of(0, 100));

// 7. Index frequently queried columns
@Table(indexes = {
    @Index(name = "idx_order_status", columnList = "status"),
    @Index(name = "idx_order_customer", columnList = "customer_id")
})

Common Interview Questions

  1. JpaRepository vs CrudRepository?
  2. JpaRepository: JPA-specific, flush, batch delete, getReference
  3. CrudRepository: Basic CRUD, database-agnostic

  4. How to handle N+1 problem?

  5. JOIN FETCH, @EntityGraph, @BatchSize, @Fetch(SUBSELECT)

  6. Lazy vs Eager loading?

  7. Lazy: Load on access (default for collections)
  8. Eager: Load immediately (default for single associations)

  9. @Transactional best practices?

  10. Use readOnly for queries
  11. Keep transactions short
  12. Understand propagation

  13. Optimistic vs Pessimistic locking?

  14. Optimistic: @Version, no DB lock, check on update
  15. Pessimistic: DB lock, @Lock annotation

  • *