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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
