Spring REST API & Data JPA
25/46
Bài 25 / 46~12 phútJPA FundamentalsLộ trình · chặng 16/21Miễn phí… lượt xem

@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:

  1. Khởi tạo đối tượng: Sử dụng reflection qua Constructor.newInstance() không tham số.
  2. Gán dữ liệu vào field: Đọc từ ResultSet và gán vào thuộc tính (qua setter hoặc field reflection).
  3. 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ậtLý 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 finalCGLIB 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 final và không có setter.
Java
// 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.

Ba hàng cùng một trục thời gian: IDENTITY thì persist với id null rồi INSERT RETURNING id nên phải chờ từng dòng, SEQUENCE thì nextval lấy sẵn block 50 id để gán trước INSERT rồi gộp một batch, UUID v7 thì app tự sinh id nên không hỏi DB lần nào

Chiến lược GenerationType.IDENTITY

Java
@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 (BIGSERIAL hoặc IDENTITY).
  • 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 SQL INSERT để 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

Java
@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ố theo allocationSize).
  • 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 INSERT thành một gói batch statement duy nhất gửi tới database.
SQL
-- 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):

Java
@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íIDENTITYSEQUENCEUUID v7
Thời điểm có IDSau khi phát lệnh INSERTTrướ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 Overhead1 round-trip mỗi bản ghi1 round-trip cho mỗi allocationSize0 round-trip để lấy ID
Phù hợp nhấtCRUD ứng dụng nhỏ, ít insertHệ thống tải ghi lớn, batch jobMicroservices, distributed architecture

Những cạm bẫy tai hại thường gặp là gì?

  1. Lệch cấu hình giữa allocationSize và DB sequence: Nếu allocationSize = 50 nhưng trong database sequence chỉ cấu hình INCREMENT 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ính duplicate key violates unique constraint.

Hai khoảng id Hibernate tự cấp sau hai lần nextval, xếp thẳng hàng để lộ phần chồng nhau: khoảng đầu là id 1 tới 50, khoảng sau là id 2 tới 51, nên dải id 2 tới 50 được cấp phát hai lần và INSERT thứ hai vỡ unique key

  1. Sử dụng GenerationType.AUTO mặc định: AUTO khiế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ị.
  2. Đá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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao Java Record không thể được sử dụng trực tiếp làm @Entity trong JPA / Hibernate?
  2. Q2
    Tại sao chiến lược GenerationType.IDENTITY lại vô hiệu hóa tính năng Batch Insert của Hibernate?
  3. Q3
    Chiế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?
  4. Q4
    UUID 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ệ?
  5. Q5
    Hiệ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.

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

Bài tiếp theo

@Column, @Enumerated và @Embeddable: Ánh Xạ Thuộc Tính