Backend

99 Ngày Spring — Ngày 22: Entity mapping cơ bản

SSite Admin
18 tháng 08, 2026 6 phút đọc 0 lượt xem
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.

Sketchnote Ngày 22: entity mapping cơ bản — @Entity/@Id, chiến lược sinh khóa IDENTITY vs SEQUENCE, @Column và quy ước snake_case

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ốn
  • Hai annotation tối thiểu: @Entity đánh dấu lớp được JPA quản lý, @Id chỉ đị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; để protected nhằ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ăng
  • Khá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.

  • AUTO kém dự đoán (Hibernate tự quyết theo DB); UUID hợ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/unique khai báo ràng buộc ngay tại entity — với ddl-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) — int không biểu diễn được NULL; 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 = false cho 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_sql là 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, price BigDecimal, createdAt) — tạo bảng tương ứng bằng SQL thủ công rồi khởi động với validate để đối chiếu.

  • Cố tình khai length = 100 trong 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ật show-sql, insert vài bản ghi và quan sát các câu nextval thưa hơn số lần insert.

  • Thêm cột nullable Integer rating — viết đoạn đọc rating ra int và 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!

S

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.

Bài viết liên quan

99 Ngày Java — Ngày 22: Wrapper class & autoboxing

Ngày 22 của 99 Ngày Java: wrapper class và autoboxing — cầu nối tự động giữa primitive và đối tượng, Integer cache với hiện tượng == đúng ở 127 sai ở 128, bẫy unboxing null gây NPE, boxing thầm lặng trong vòng lặp, và cách chọn giữa parseInt với valueOf.

18 thg 8, 20266 phút0
99 Ngày Java — Ngày 21: StringBuilder & xử lý chuỗi nâng cao

Ngày 21 của 99 Ngày Java: vì sao nối chuỗi bằng + trong vòng lặp có chi phí bình phương, StringBuilder với capacity và bộ API thường dùng, String pool — lý do == đôi khi tình cờ đúng nhưng equals mới là quy tắc, cùng String.format và text block cho chuỗi nhiều dòng dễ đọc.

17 thg 8, 20266 phút11
99 Ngày Spring — Ngày 21: JPA & Hibernate tổng quan

Ngày 21 của 99 Ngày Spring, mở đầu giai đoạn Spring Data JPA: impedance mismatch giữa đối tượng và bảng quan hệ, phân vai JPA (đặc tả) — Hibernate (cài đặt) — Spring Data JPA (tầng tiện ích), EntityManager với persistence context, và lựa chọn ddl-auto đúng cho từng môi trường.

17 thg 8, 20267 phút12