Entity Lifecycle và equals/hashCode Trong JPA
Khám phá 4 trạng thái vòng đời entity, 7 lifecycle callbacks, cạm bẫy Lombok @Data và kỹ thuật cài đặt equals/hashCode an toàn với Hibernate Proxy.
TL;DR: Entity JPA trải qua 4 trạng thái vòng đời: Transient, Managed, Detached, và Removed. JPA hỗ trợ 7 lifecycle callbacks (@PrePersist, @PostPersist, @PreUpdate...) để can thiệp vào các mốc thay đổi. Cài đặt equals/hashCode trên JPA Entity là bài toán hóc búa vì ID bị null trước khi persist và Hibernate sử dụng CGLIB proxy cho lazy load. Tuyệt đối không dùng Lombok @Data cho Entity.
Bốn trạng thái vòng đời của Entity hoạt động như thế nào?
Mọi đối tượng JPA tại một thời điểm đều nằm trong 1 trong 4 trạng thái được quản lý bởi persistence context:
| Trạng thái | Định danh ID | Được theo dõi (Tracking)? | Mô tả |
|---|---|---|---|
| Transient | null | Không | Đối tượng mới khởi tạo qua new, chưa lưu xuống DB. |
| Managed | Đã có | Có | Nằm trong persistence context, Dirty Checking kích hoạt. |
| Detached | Đã có | Không | Transaction đã đóng, thay đổi setter không tự động flush. |
| Removed | Đã có | Có | Đã đánh dấu xóa qua em.remove(), chờ phát lệnh DELETE. |
Chỉ khi Entity ở trạng thái Managed, cơ chế Dirty Checking mới theo dõi thay đổi để tự động phát sinh câu lệnh SQL UPDATE.
Bảy Lifecycle Callbacks can thiệp vào vòng đời ra sao?
JPA cung cấp 7 annotation đánh dấu method void nội bộ trong entity:
@Entity
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Instant createdAt;
private Instant updatedAt;
@PrePersist
protected void onCreate() {
// Chay truoc INSERT khi id van con null
this.createdAt = Instant.now();
}
@PreUpdate
protected void onUpdate() {
// Chay truoc UPDATE khi Dirty Checking phat hien thay doi
this.updatedAt = Instant.now();
}
@PostPersist
protected void afterInsert() {
// Chay sau INSERT, id da duoc DB gan thanh cong
}
@PostLoad
protected void afterLoad() {
// Chay sau khi Hibernate load entity tu DB len bo nho
}
}

Các callback như @PrePersist, @PreUpdate chạy trực tiếp bên trong database transaction. Nếu gọi HTTP request hoặc network I/O, transaction sẽ bị treo, giữ kết nối database pool gây nghẽn hệ thống.
Tại sao cài đặt equals/hashCode cho Entity lại phức tạp?
Trong Java thông thường, equals và hashCode được sinh tự động dựa trên tất cả các trường dữ liệu. Tuy nhiên, với JPA Entity, hai trở ngại lớn xuất hiện:
- Khóa chính ID bị null trước khi lưu: Khi entity ở trạng thái Transient,
idbằngnull. Nếu tính hashCode theo ID, giá trị hash ban đầu sẽ là 0. - Hibernate CGLIB Proxy: Đối tượng tải qua lazy load là một proxy con (subclass), không phải class gốc.
Thảm họa HashSet mất dấu Entity
Hãy xem xét kịch bản sau nếu hashCode() tính theo id:
Set<Project> set = new HashSet<>();
Project project = new Project("TaskFlow"); // id = null -> hashCode = 0
set.add(project); // Luu vao Bucket 0
projectRepository.save(project); // DB sinh id = 42 -> hashCode bien thanh 42!
boolean exists = set.contains(project); // HashSet tim tai Bucket 42 -> Tra ve FALSE!
Mặc dù đối tượng vẫn nằm trong HashSet, nhưng set.contains() trả về false vì hash code đã thay đổi giữa chừng.

Triển khai equals và hashCode chuẩn mực như thế nào?
Công thức an toàn và được Hibernate khuyến nghị:
- Dùng
instanceoftrongequals: Tương thích với CGLIB Proxy subclass của Hibernate (thay vì so sánhgetClass() == o.getClass()vốn sẽ trả vềfalsevới proxy). - So sánh ID an toàn: Chỉ coi hai entity bằng nhau nếu cả hai đều có ID khác null và bằng nhau.
- Trả về hằng số cố định trong
hashCode(): Đảm bảo bucket trongHashSetkhông bao giờ bị thay đổi trước và sau khi persist.
@Entity
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Project other)) return false; // Tuong thich Hibernate Proxy
return id != null && id.equals(other.id);
}
@Override
public int hashCode() {
return getClass().hashCode(); // Hang so co dinh cho ca class
}
}
Lưu ý: Trả về getClass().hashCode() khiến mọi instance của Project rơi vào cùng bucket trong Set, lookup chuyển thành O(N). Nhưng trong thực tế, các bộ Set chứa entity thường có kích thước rất nhỏ (vài chục phần tử), trade-off này hoàn toàn đáng giá để bảo toàn tính toàn vẹn dữ liệu.
Vì sao không bao giờ được dùng Lombok @Data trên Entity?
Annotation @Data của Lombok là nguyên nhân hàng đầu gây ra các lỗi runtime bí ẩn trong JPA:
- Sinh
equalsvàhashCodetrên mọi field: Vi phạm nguyên tắc bất biến của HashSet và gây kích hoạt Lazy Loading ngoài ý muốn khi truy cập field quan hệ. - Sinh
toString()bao gồm cả quan hệ hai chiều: Khi Entity A trỏ tới B và B trỏ ngược lại A, gọitoString()sẽ gây ra ngoại lệStackOverflowError(vòng lặp vô tận). - Mặc định sinh constructor không mong muốn: Thiếu no-arg constructor cần thiết cho Hibernate.
// KHUYEN CAO: Thay @Data bang cac annotation cu the
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Project { ... }
Những cạm bẫy phổ biến cần tránh là gì?
- Gọi
getClass() == o.getClass()trong equals: Sẽ trả vềfalsekhi một bên là Hibernate Lazy Proxy, phá vỡ logic so sánh. - Gọi network I/O trong @PrePersist: Giữ connection pool quá lâu, làm cạn kiệt tài nguyên database.
- Dựa vào ID trong hashCode: Khiến đối tượng bị biến mất khỏi
SethoặcMapsau khi gọirepository.save().
Tự kiểm tra kiến thức
- Q1Tại sao trong phương thức equals() của JPA Entity nên dùng 'o instanceof MyEntity' thay vì 'getClass() == o.getClass()'?
- Q2Hiện tượng gì xảy ra nếu phương thức hashCode() của Entity được tính toán dựa trên trường ID tự tăng?
- Q3Tại sao không nên sử dụng annotation @Data của Lombok trên các JPA Entity?
- Q4Callback nào trong 7 Lifecycle Callbacks của JPA được kích hoạt ngay trước khi câu lệnh SQL INSERT được flush?
- Q5Tại sao việc gọi một dịch vụ bên ngoài (External REST API) bên trong @PrePersist hoặc @PreUpdate lại là một thói quen xấu?
Bài tiếp theo: Tổng Kết Module 04: JPA Fundamentals
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
@Column, @Enumerated và @Embeddable: Ánh Xạ Thuộc Tính
Bài tiếpTổng Kết Module 04: Nền Tảng JPA và Ánh Xạ Thực Thể
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 6/20 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này