99 Ngày Spring — Ngày 22: Entity mapping cơ bản
Bản đồ tổng quan đã có (Ngày 21) — hôm nay ta viết entity đầu tiên: lớp Java ánh xạ một bảng PostgreSQL. Ba nhóm quyết định của bài: bộ khung @Entity + @Id với các ràng buộc JPA đặt ra cho lớp, chiến lược sinh khóa chính — lựa chọn ảnh hưởng thẳng đến hiệu năng insert, và @Column với các quy tắc ánh xạ kiểu — nơi bài wrapper của series Java hôm nay xuất hiện đúng lúc.

Entity đầu tiên — @Entity, @Id và các ràng buộc
@Entity // JPA: lớp này ánh xạ MỘT bảng@Table(name = "posts") // tên bảng — mặc định là tên lớppublic class Post {
@Id // khóa chính
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title; // ánh xạ cột cùng tên (title)
protected Post() { } // JPA BẮT BUỘC no-arg constructor —
// lý do entity là CLASS, không phải
// record (bài Java Ngày 20)
public Post(String title) {
this.title = title;
}
// getter / setter — Hibernate cần thay đổi trạng thái (dirty checking)
}
// Khởi động với ddl-auto: validate (Ngày 21):// thiếu bảng posts → fail ngay — đúng hành vi mong muốnHai annotation tối thiểu:
@Entityđánh dấu lớp được JPA quản lý,@Idchỉ định khóa chính — thiếu một trong hai, ứng dụng fail ngay khi khởi động.JPA yêu cầu no-arg constructor (Hibernate khởi tạo entity bằng reflection) và setter cho dirty checking — chính hai yêu cầu này trả lời câu hỏi treo từ Java Ngày 20: vì sao entity không là record; để
protectednhằm tránh bị dùng nhầm trong code nghiệp vụ.Khai báo
@Table(name = ...)tường minh giúp entity đổi tên lớp không kéo theo đổi tên bảng — hợp đồng với DB tách khỏi tên trong code.
Chiến lược sinh khóa chính
// Chiến lược sinh khóa chính — lựa chọn quan trọng hơn vẻ ngoài:
@GeneratedValue(strategy = GenerationType.IDENTITY)// cột auto-increment của DB — id chỉ có SAU khi INSERT// → Hibernate phải gửi từng INSERT ngay, KHÔNG gom batch được
@GeneratedValue(strategy = GenerationType.SEQUENCE)@SequenceGenerator(name = "post_seq",
sequenceName = "post_seq",
allocationSize = 50)// sequence của DB — lấy trước một dải 50 id,// entity có id TRƯỚC khi INSERT → batch insert hiệu quả// → KHUYẾN NGHỊ với PostgreSQL
// GenerationType.AUTO — Hibernate tự chọn theo DB: kém dự đoán, nên tránh// GenerationType.UUID (JPA 3.1) — khóa UUID: hợp hệ phân tán,// đổi lại index lớn hơn số tự tăngKhác biệt cốt lõi IDENTITY vs SEQUENCE nằm ở thời điểm có id: IDENTITY phải INSERT xong mới biết id → Hibernate mất khả năng gom batch; SEQUENCE lấy trước dải id (
allocationSize) → insert hàng loạt hiệu quả.Với PostgreSQL, SEQUENCE là khuyến nghị chuẩn — IDENTITY vẫn chấp nhận được cho ứng dụng nhỏ, nhưng chọn đúng từ đầu tránh một lần migration đổi chiến lược về sau.
AUTOkém dự đoán (Hibernate tự quyết theo DB);UUIDhợp hệ phân tán và tránh lộ số lượng bản ghi qua id tuần tự — đổi lại index lớn hơn.
@Column — ràng buộc và ánh xạ kiểu
@Column(name = "title", nullable = false, length = 200)private String title; // NOT NULL + VARCHAR(200)
@Column(unique = true)private String slug; // ràng buộc UNIQUE
@Column(columnDefinition = "TEXT")private String content; // kiểu cột tùy chỉnh
@Column(name = "created_at", updatable = false)private LocalDateTime createdAt; // ghi một lần — câu UPDATE không đụng tới
@Column(name = "view_count")private Integer viewCount; // WRAPPER cho cột NULLABLE —
// primitive int không biểu diễn được NULL
// (đúng bài Java hôm nay!)
// Ánh xạ kiểu thường gặp:// String → VARCHAR · LocalDateTime → TIMESTAMP · BigDecimal → NUMERIC// boolean → BOOLEAN · enum → @Enumerated(EnumType.STRING) (Ngày 39)nullable/length/uniquekhai báo ràng buộc ngay tại entity — vớiddl-auto: validate, chúng là tài liệu được kiểm chứng đối chiếu với schema thật.Chi tiết đáng giá nhất của bài: cột nullable dùng wrapper (
Integer viewCount) —intkhông biểu diễn đượcNULL; và khi đọc giá trị đó ra tính toán, bẫy unboxing null của bài Java hôm nay chờ sẵn.updatable = falsecho các cột ghi-một-lần (createdAt) — cơ chế audit tự động hóa hoàn chỉnh hẹn Ngày 29.
Quan sát SQL & quy ước đặt tên
# Bộ ba cấu hình quan sát khi phát triển:spring:jpa:
show-sql: true # in SQL mà Hibernate sinh ra
properties:
hibernate:
format_sql: true # xuống dòng, thụt lề cho dễ đọc
hibernate:
ddl-auto: validate # entity lệch schema → fail ngay (Ngày 21)
# Quy ước đặt tên mặc định (physical naming strategy):# camelCase → snake_case: createdAt → created_at# PascalCase → snake_case: PostTag → post_tag# Giữ nguyên mặc định — khớp chuẩn đặt tên phổ biến của PostgreSQL,# và là lý do @Column(name = "created_at") ở trên chỉ mang tính tường minh.show-sql+format_sqllà thói quen phát triển đáng giữ từ entity đầu tiên: nhìn thấy SQL Hibernate sinh ra là cách nhanh nhất phát hiện truy vấn bất thường — chuẩn bị cho bài N+1 (Ngày 27).Quy ước camelCase → snake_case là mặc định của Spring Boot — thuận theo nó thì phần lớn
@Column(name = ...)chỉ mang tính tường minh, không bắt buộc.
Bài tập nhỏ
Viết entity
Book(id, title, isbn unique, priceBigDecimal, createdAt) — tạo bảng tương ứng bằng SQL thủ công rồi khởi động vớivalidateđể đối chiếu.Cố tình khai
length = 100trong khi cột thật làVARCHAR(200)— đọc thông báo lỗi validate và sửa lại cho khớp.Đổi IDENTITY sang SEQUENCE với
allocationSize = 50— bậtshow-sql, insert vài bản ghi và quan sát các câunextvalthưa hơn số lần insert.Thêm cột nullable
Integer rating— viết đoạn đọc rating raintvà xử lý null đúng cách (bài Java hôm nay).
Kết luận
Entity đầu tiên đã đứng vững trên ba quyết định có căn cứ: bộ khung @Entity/@Id với no-arg constructor lý giải vì sao entity là class, SEQUENCE thay IDENTITY để giữ khả năng batch insert trên PostgreSQL, và @Column biến ràng buộc schema thành tài liệu được validate kiểm chứng — cùng quy tắc wrapper-cho-cột-nullable nối hai series trong một ngày. Ngày 23 ta trao entity cho Spring Data JPA repository: JpaRepository với bộ CRUD có sẵn và cơ chế derived query sinh truy vấn từ tên method. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


