Đang tải...

Những hiểu lầm ngớ ngẩn về JPA mà tôi đã từng mắc khi đi làm

13/05/2026
9 phút đọc
Những hiểu lầm ngớ ngẩn về JPA mà tôi đã từng mắc khi đi làm
# Mở đầu Trước đây, khi vừa ra trường và gõ những dòng code enterprise đầu tiên, đã có một thời gian tôi nghĩ rằng bản thân đã thực sự hiểu tường tận...

Mở đầu

Trước đây, khi vừa ra trường và gõ những dòng code enterprise đầu tiên, đã có một thời gian tôi nghĩ rằng bản thân đã thực sự hiểu tường tận JPA. Tôi dùng save, saveAll, repository, transaction mỗi ngày. Gần như mọi project đều sử dụng đến nó. API vẫn chạy ổn, dữ liệu vẫn lưu được, leader quá bận nên chỉ review code qua loa miễn nó chay được là được :)). Nên tôi đã nghĩ rằng tôi hiểu nó, và sai lầm hơn khi nghĩ rằng mình thành thạo nó.

Tôi cứ thế làm và làm. Không sai không biết không sửa. Mọi thứ vẫn ổn cho đến khi nó không còn ổn nữa. Sau vài lần debug toát mồ hôi, tôi nhận ra mình chỉ đang dùng JPA theo bản năng chứ chưa thực sự hiểu nó hoạt động như thế nào.

Đây là một số sai lầm mà tôi từng mắc trong quá trình tự cao tự đại về cái trình độ code theo bản năng của tôi.

1. delete() rồi save() mà vẫn bị duplicate key

Nói ra thì quê nhưng tôi thực sự đã từng nghĩ như này. delete(instant) -> save(newInstant). Đấy logic trong bộ não bé nhỏ của tôi đã từng như này. Xóa cái cũ đi rồi thêm cái mới vào. Cái cũ xóa đi rồi thì làm gì có chuyện lỗi đâu.

Mọi chuyện diễn ra ban đầu rất em đẹp. Cho đến khi đồng nghiệp của tôi thêm constraint vào. Bùm function tôi viết gần như là tê liệt. QC hú hét như bố đẻ em bé. Cũng may chỉ là môi trường dev nên tôi vẫn còn cơ hội tìm hiểu và sửa sai. Và sau thời gian tìm hiểu tôi đã hiểu ra tôi sai ở đâu :(. Tôi đã viết vào repository của mình như này:

void deleteByCaseId(String caseId);

Và tôi dùng nó kiểu như này

@Transactional
public void replace(String caseId) {
 repo.deleteByCaseId(caseId);

 repo.saveAll(newEntities);
}

Yup. Delete xong save. Perfect flow :v: . Nhưng mà đời đâu như là mơ. Thằng Hibernate nó chưa bắn DELETE xuống DB ngay. Nó chỉ xếp hàng câu DELETE trong persistence context/action queue. Sau đó saveAll() thêm INSERT vào queue. Đến lúc flush, thứ tự SQL có thể khiến INSERT chạy khi dữ liệu cũ vẫn còn, nên dính unique key.

Nhưng không sao, thất bại là mẹ thành công tôi đã tìm ra cách fix với api thần thánh flush(). flush() giúp ép Hibernate gửi SQL xuống DB sớm hơn, nhưng không commit transaction. Nếu sau đó method throw exception thì cả DELETE và INSERT vẫn rollback.

@Transactional
public void replace(String caseId) {
 repo.deleteByCaseId(caseId);
 repo.flush();

 repo.saveAll(newEntities);
}

Dễ dàng nhưng tràn đầy uy lực. Hoặc bạn cũng có thể sử dụng EntityManager để giải quyết case này

@Transactional
public void replace(String caseId) {
 repo.deleteByCaseId(caseId);
 entityManager.flush();

 repo.saveAll(newEntities);
}

Và hãy nhớ flush() != commit. Đừng như tôi :(

2. N+1 Query - vòng lặp vô hại giết production

Từ hồi trong ghế nhà trường. Tôi đã từng có những dòng code cute như thế này

List<Order> orders = orderRepository.findAll();

for (Order order : orders) {
 System.out.println(order.getCustomer().getName());
}

Nhìn qua thì cũng oách mà.

Không exception.

Không warning.

Code clean.

Chạy local mượt.

Ơ thế tại sao mình lại không lấy nó để mang lên công ty code nhỉ? Nhỉiiiii...

Ý tưởng đấy hay đấy. Nhưng chưa kịp golive mới SIT thôi mớ data to đùng đã khiến sản phẩm lag lòi rồi :(. Và N+1 query chính là thủ phạm. Vậy trong đoạn code trên N+1 ở đâu. Thấy mỗi cái findAll() thôi mà? Và đây, chính là chỗ gây hiểu nhầm lớn

@Entity
public class Order {

 @ManyToOne(fetch = FetchType.LAZY)
 private Customer customer;
}

Khi gọi findAll() thì sẽ có một câu SQL được sinh ra để lấy tất cả bản ghi Order SELECT * FROM orders;. Sau đó mỗi khi getCustomer() lại bắt đầu tạo ra các cậu SQL dạng

SELECT * FROM customer WHERE id = 1;
SELECT * FROM customer WHERE id = 2;
SELECT * FROM customer WHERE id = 3;
...

Tổng cộng ta có:

1 query lấy orders
+
N query lấy customer

=> đây chính là thứ mà người ta gọi là N+1 problems

Vì sao Hibernate lại làm vậy nhỉ?

Đó là do Lazy loading

@ManyToOne(fetch = FetchType.LAZY)

Hibernate không load customer ngay. Nó chỉ tạo một proxy object.

Chỉ khi:

order.getCustomer()

thì mới query database.

Ý tưởng ban đầu của LAZY là tốt:

  • tiết kiệm memory
  • tránh join không cần thiết

Nhưng trong loop lớn thì thành thảm họa.

Điều đáng sợ: code nhìn hoàn toàn vô hại

Đây là lý do N+1 nguy hiểm.

Đoạn này:

orders.stream()
 .map(o -> o.getCustomer().getName())
 .toList();

trông rất functional, rất clean.

Nhưng có thể bắn 5000 query, 10000 query hoặc hơn

Không ai nhìn bằng mắt thường mà biết ngay được.

Vậy làm thế nào để phát hiện N+1

Ta có thể bật log SQL lên. Local thôi nhé. Để ở product rác log sếp tôi đấm á :(

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG

Nếu thấy log kiểu:

SELECT * FROM customer WHERE id=?
SELECT * FROM customer WHERE id=?
SELECT * FROM customer WHERE id=?
...

spam liên tục => gần như chắc chắn dính N+1.

Ơ thế giải quyết vấn đề này như nào? Chả nhẽ lại xài EAGER à :)

  1. Dùng FETCH JOIN Đây là cách phổ biến nhất đối với tôi :3
@Query("""
 SELECT o
 FROM Order o
 JOIN FETCH o.customer
""")
List<Order> findAllWithCustomer();

Hibernate sẽ query kiểu như này:

SELECT *
FROM orders o
JOIN customer c ON o.customer_id = c.id

=> quàooo. Chỉ còn 1 query thôi

  1. Projection - cách tôi thích dùng hơn ở API read-only

Nếu chỉ cần để read thôi thì cách này với tôi là best

public interface OrderProjection {
 String getOrderCode();
 String getCustomerName();
}

Query:

@Query("""
 SELECT
 o.code as orderCode,
 c.name as customerName
 FROM Order o
 JOIN o.customer c
""")
List<OrderProjection> findData();

Ưu điểm:

  • nhanh hơn entity
  • ít memory
  • tránh lazy loading
  • tránh dirty checking

Ơ nhưng mà đừng xài EAGER để fix N+1

@ManyToOne(fetch = FetchType.EAGER)

Chuẩn mà nhỉ. Data load hết vào. Không query nữa thì làm sao mà có N+1. Easy

Nhưng mà bạn tôi ơi EAGER:

  • có thể tạo query cực lớn
  • load dữ liệu thừa
  • gây cartesian explosion
  • memory tăng mạnh

=> chỉ chuyển từ vấn đề này sang vấn đề khác.

3. saveAll() không tự batch insert

Trước đây tôi cứ nghĩ. Cái nào mà save nhiều thì cứ vã saveAll vào. Auto có batch. Nhanh không ấy mà. Ấy thế mà tôi lại sai.

Nếu bạn không bật config này lên

spring.jpa.properties.hibernate.jdbc.batch_size=1000
spring.jpa.properties.hibernate.order_inserts=true

thì rất có thể khi bạn dùng saveAll(10000 nghìn entities) đã có một người hùng thầm lặng tạo 10000 câu insert cho bạn và cần mẫn chạy 10000 câu đó hộ bạn đấy :v:

Kết luận

JPA rất mạnh, nhưng cũng rất “nguy hiểm” nếu chỉ dùng theo cảm tính.

Điều khó nhất của JPA không phải syntax.

Mà là hiểu:

  • khi nào entity đang được quản lý
  • lúc nào SQL thực sự chạy
  • transaction ảnh hưởng ra sao
  • và framework đang “tự động” làm gì phía sau lưng mình

Ựa bài đến đây với tôi là dài rồi. Dạo này vibe code nhiều đù hết người. Động não viết một bài cũng oải. Có thể sẽ có phần 2 nha. Mong mọi người ủng hộ. Do kinh nghiệm và trình độ bản thân chưa được phong phú, trong bài viết nếu có sai xót nào mong nhận được sự chỉ điểm của mọi người phía comment. Cảm ơn mọi người đã dành thời gian đọc bài

📚 Nguồn: Viblo

Chia sẻ bài viết

Cần tư vấn?

Liên hệ với chúng tôi để được hỗ trợ

Liên hệ ngay

Bài viết liên quan

🤖 Building Social Games with AI — The Practitioner's Guide 📖
14/05/2026

🤖 Building Social Games with AI — The Practitioner's Guide 📖

> A comprehensive, opinionated, actionable guide for **using AI to build, ship, and operate social games** in the lineage covered by [🌾 The Social Games Playbook 🎮](https://dev.to/truongpx396/th...

Đọc thêm
qs88nl5001
14/05/2026

qs88nl5001

qs88 mang đến sân chơi giải trí trực tuyến chuyên nghiệp với hệ thống vận hành ổn định cùng tốc độ xử lý nhanh chóng. Người tham gia dễ dàng tiếp cận h...

Đọc thêm
Claude đã giúp lưu lượng truy cập website của tôi tăng gấp đôi như thế nào ?
14/05/2026

Claude đã giúp lưu lượng truy cập website của tôi tăng gấp đôi như thế nào ?

> Bài viết này được tổng hợp và diễn giải lại từ bài gốc ["Claude is Doubling My Website Traffic"](https://nick-nolan.medium.com/claude-is-doubling-my-website-traffic-1a26a793b...

Đọc thêm

Bắt đầu dự án của bạn

Hãy để Flash Dev đồng hành cùng bạn

Liên hệ ngay