Why Misusing JPA findById Hurts Performance and Causes Critical Pitfalls

The article dissects how the default JpaRepository findById method can trigger unnecessary SELECT queries, create foreign‑key violations under concurrency, and degrade performance, then demonstrates three concrete workarounds—including getReferenceById, manual entity construction, and pessimistic read locking—to eliminate these issues.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Why Misusing JPA findById Hurts Performance and Causes Critical Pitfalls

1. Introduction

In Spring Data JPA, JpaRepository.findById is the standard way to load an entity by its primary key. Although it looks simple, the method can hide several performance and concurrency pitfalls.

It may trigger an unnecessary SELECT query, adding load to the database.

In multithreaded scenarios the returned managed entity can cause foreign‑key or consistency problems when the underlying data changes.

2. Practical Example

2.1 Setup

Two JPA entities with a bidirectional one‑to‑many relationship are defined:

@Entity
@Table(name = "a_order")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String sno;
    private BigDecimal total;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "order")
    private Set<OrderItem> items = new HashSet<>();
}

@Entity
@Table(name = "a_order_item")
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;
    @ManyToOne(cascade = CascadeType.REFRESH, fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}

2.2 Performance Issue (Saving OrderItem)

A typical service method uses findById to obtain the Order before persisting an OrderItem:

@Transactional
public void addOrderItem(OrderItem item, Long orderId) {
    item.setOrder(this.orderRepository.findById(orderId)
        .orElseThrow(() -> new RuntimeException("Invalid order")));
    this.orderItemRepository.save(item);
}

The corresponding unit test inserts an OrderItem for orderId = 1L. The generated SQL shows an extra SELECT that loads the whole Order entity, even though only the identifier is needed.

2.3 Solution 1 – Use getReferenceById

Replacing findById with getReferenceById returns a lazy proxy without hitting the database:

@Transactional
public void addOrderItem(OrderItem item, Long orderId) {
    item.setOrder(this.orderRepository.getReferenceById(orderId));
    this.orderItemRepository.save(item);
}

The resulting SQL contains only the INSERT for OrderItem, eliminating the unnecessary SELECT.

2.4 Solution 2 – Manually Create a Stub Order

Another approach is to instantiate an Order with only the identifier set:

@Transactional
public void addOrderItem(OrderItem item, Long orderId) {
    item.setOrder(new Order(orderId));
    this.orderItemRepository.save(item);
}

This also results in a single INSERT statement.

2.5 Concurrency Issue with findById

A more complex test introduces a race condition: one thread calls addOrderItem (which uses findById) and pauses for three seconds; meanwhile another thread deletes the same Order. When the first thread resumes, the persisted OrderItem references a non‑existent Order, violating the foreign‑key constraint.

@Transactional
public void addOrderItem(OrderItem item, Long orderId) {
    Order order = this.orderRepository.findById(orderId)
        .orElseThrow(() -> new RuntimeException("Invalid order"));
    TimeUnit.SECONDS.sleep(3);
    item.setOrder(order);
    this.orderItemRepository.save(item);
}

@Transactional
public void deleteOrder(Long orderId) {
    this.orderRepository.deleteById(orderId);
}

The test output shows a foreign‑key error.

2.6 Fix – Pessimistic Read Lock on findById

By adding a pessimistic read lock to the repository method, the SELECT acquires a FOR SHARE lock, preventing concurrent deletions while allowing reads:

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Lock(LockModeType.PESSIMISTIC_READ)
    Optional<Order> findById(Long id);
}

Re‑running the concurrency test now succeeds, as shown by the successful SQL output.

3. Conclusion

Using findById indiscriminately can cause unnecessary database round‑trips and introduce foreign‑key violations under concurrent access. Replacing it with getReferenceById, constructing a lightweight entity with only the ID, or applying a pessimistic read lock are effective strategies to eliminate the performance loss and avoid critical concurrency bugs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceconcurrencySpring Bootpessimistic-lockjpafindbyid
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.