@Entity và @Id: POJO Contract và Chiến Lược Khóa Chính
Tìm hiểu yêu cầu POJO của @Entity và so sánh ba chiến lược sinh khóa IDENTITY, SEQUENCE, UUID v7 dưới góc nhìn hiệu năng batch insert của Hibernate.
TL;DR: @Entity yêu cầu ba ràng buộc kỹ thuật: no-arg constructor, class không được final, và field mutable. Java record vi phạm các điều kiện này nên chỉ dùng cho DTO. Về khóa chính, @GeneratedValue cung cấp ba chiến lược: IDENTITY (DB sinh sau INSERT, chặn batching), SEQUENCE (Hibernate lấy trước block ID, tối ưu batching), và UUID v7 (app sinh local theo timestamp, tối ưu distributed system và B-tree index).
Vì sao JPA bắt buộc Entity phải là Mutable POJO thay vì Record?
Trước khi đọc annotation, hãy xem xét cách Hibernate tải dữ liệu từ database:
- Khởi tạo đối tượng: Sử dụng reflection qua
Constructor.newInstance()không tham số. - Gán dữ liệu vào field: Đọc từ
ResultSetvà gán vào thuộc tính (qua setter hoặc field reflection). - Tạo CGLIB Dynamic Proxy: Kế thừa entity class để chặn getter nhằm phục vụ tính năng Lazy Loading.
Từ cơ chế đó, JPA đặt ra ba yêu cầu kỹ thuật:
| Yêu cầu kỹ thuật | Lý do phía Hibernate |
|---|---|
No-arg constructor (public / protected) | Reflection gọi newInstance() khi hydrate đối tượng từ hàng dữ liệu. |
Class không được final | CGLIB bắt buộc phải kế thừa class để tạo proxy phục vụ lazy loading. |
| Thuộc tính mutable (có setter hoặc non-final) | Cần cập nhật giá trị sau khi đối tượng đã khởi tạo. |
Tại sao Java Record vi phạm cả ba yêu cầu trên?
Java Record (từ Java 16+) được thiết kế với tính chất bất biến (immutable):
- Record class ngầm định là
final, CGLIB không thể kế thừa (HibernateException: cannot subclass final class). - Record chỉ có canonical constructor (đầy đủ tham số), không có no-arg constructor.
- Các component của record là
private finalvà không có setter.
// Entity (Database Layer) - Bat buoc dung Class
@Entity
@Table(name = "projects")
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
protected Project() {} // JPA require no-arg constructor
public Project(String name) { this.name = name; }
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
// DTO (API Layer) - Su dung Record bat bien
public record ProjectDto(Long id, String name) {
public static ProjectDto from(Project p) {
return new ProjectDto(p.getId(), p.getName());
}
}
Các chiến lược @GeneratedValue khác nhau như thế nào?
Annotation @Id đánh dấu khóa chính. Cùng với nó, @GeneratedValue quyết định thời điểm và nơi phát sinh giá trị ID.

Chiến lược GenerationType.IDENTITY
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- Cơ chế: Dựa vào cột tự tăng của cơ sở dữ liệu (
BIGSERIALhoặcIDENTITY). - Hạn chế lớn nhất: Hibernate không thể gom lô (batch insert). Khi gọi
persist(), Hibernate bắt buộc phải phát ngay lệnh SQLINSERTđể lấy ID trả về phục vụ việc quản lý entity trong persistence context. Nếu lưu 1000 entity, ứng dụng phải thực hiện đúng 1000 network round-trip.
Chiến lược GenerationType.SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "proj_seq")
@SequenceGenerator(name = "proj_seq", sequenceName = "projects_id_seq", allocationSize = 50)
private Long id;
- Cơ chế: Hibernate gọi lệnh
SELECT nextval('projects_id_seq')để lấy trước một dải ID (ví dụ 50 số theoallocationSize). - Tối ưu Batch Insert: Vì đã biết trước ID ở phía ứng dụng, Hibernate có thể gán sẵn ID cho entity và gom nhiều câu lệnh
INSERTthành một gói batch statement duy nhất gửi tới database.
-- DDL dong bo: INCREMENT phai bang allocationSize
CREATE SEQUENCE projects_id_seq START WITH 1 INCREMENT BY 50;
Chiến lược UUID v7 trên Hibernate 6.6+
UUID truyền thống (UUID v4) mang tính ngẫu nhiên hoàn toàn. Khi chèn vào chỉ mục B-tree của database, nó gây ra hiện tượng phân mảnh trang (page split) nghiêm trọng, làm suy giảm hiệu năng ghi.
UUID v7 giải quyết triệt để vấn đề này nhờ chứa timestamp ở phần đầu (time-ordered):
@Id
@UuidGenerator(style = UuidGenerator.Style.TIME)
private UUID id;
- Ưu điểm: Ứng dụng tự sinh khóa phía client mà không cần truy vấn database, hỗ trợ hoàn hảo cho hệ thống phân tán (microservices) và bảo toàn tính liền mạch của B-tree index.
Bảng so sánh các chiến lược sinh khóa chính
| Tiêu chí | IDENTITY | SEQUENCE | UUID v7 |
|---|---|---|---|
| Thời điểm có ID | Sau khi phát lệnh INSERT | Trước INSERT (nhờ pre-fetch) | Ngay khi khởi tạo phía Java |
| Hỗ trợ Batch Insert | ❌ Không hỗ trợ | ✅ Hỗ trợ rất tốt | ✅ Hỗ trợ tối đa |
| Network Overhead | 1 round-trip mỗi bản ghi | 1 round-trip cho mỗi allocationSize | 0 round-trip để lấy ID |
| Phù hợp nhất | CRUD ứng dụng nhỏ, ít insert | Hệ thống tải ghi lớn, batch job | Microservices, distributed architecture |
Những cạm bẫy tai hại thường gặp là gì?
- Lệch cấu hình giữa allocationSize và DB sequence:
Nếu
allocationSize = 50nhưng trong database sequence chỉ cấu hìnhINCREMENT BY 1, Hibernate sẽ tính toán sai dải ID local, dẫn tới việc sinh ra các ID trùng lặp và gây lỗi vi phạm khóa chínhduplicate key violates unique constraint.

- Sử dụng GenerationType.AUTO mặc định:
AUTOkhiến Hibernate tự lựa chọn chiến lược theo dialect. Trên nhiều database, Hibernate sẽ dùng chung một bảng sequence toàn cục duy nhất (hibernate_sequence), dẫn đến ID bị phân mảnh và khó quản trị. - Đánh dấu Entity là final: Khiến CGLIB không thể tạo lớp con kế thừa để quản lý lazy loading, dẫn đến ứng dụng dừng hoạt động ngay khi khởi động.
Tự kiểm tra kiến thức
- Q1Tại sao Java Record không thể được sử dụng trực tiếp làm @Entity trong JPA / Hibernate?
- Q2Tại sao chiến lược GenerationType.IDENTITY lại vô hiệu hóa tính năng Batch Insert của Hibernate?
- Q3Chiến lược GenerationType.SEQUENCE giúp tối ưu hóa hiệu năng chèn dữ liệu thông qua cơ chế nào?
- Q4UUID v7 mang lại ưu điểm vượt trội nào so với UUID v4 khi làm khóa chính trong cơ sở dữ liệu quan hệ?
- Q5Hiện tượng gì sẽ xảy ra nếu cấu hình allocationSize = 50 trong mã nguồn nhưng database sequence lại tạo với INCREMENT BY 1?
Bài tiếp theo: @Column, @Enumerated và @Embeddable: Ánh Xạ Thuộc Tính Nâng Cao
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.
EntityManager và JPQL: Tầng Thực Thi Lõi Trong JPA
Bài tiếp@Column, @Enumerated và @Embeddable: Ánh Xạ Thuộc Tính
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 4/20 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này