Spring Boot/Entity mapping — @Entity, @Id, @GeneratedValue, naming strategy
~24 phútSpring Data JPAMiễn phí

Entity mapping — @Entity, @Id, @GeneratedValue, naming strategy

Entity là Java class map sang DB table. Bài này bóc tách @Entity requirements (no-arg ctor, mutable, equals/hashCode), @Id strategies (IDENTITY/SEQUENCE/UUID), @Column/@Table customization, naming strategy SnakeCaseStrategy, @Embeddable, @Enumerated, lifecycle callbacks.

Bài 01 đã giới thiệu JPA spec. Bài này bóc cụ thể: làm sao biến Java class thành database table? Annotation nào? Quy tắc gì?

Hiểu sau bài này: vì sao record không work với JPA, sự khác biệt IDENTITY vs SEQUENCE vs UUID, naming strategy mặc định Hibernate, cách map enum/value object, và lifecycle callback (@PrePersist, @PostLoad).

1. @Entity — yêu cầu tối thiểu

@Entity
@Table(name = "projects")
public class Project {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String name;

    @Column(length = 500)
    private String description;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private ProjectStatus status;

    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    // 1. No-arg constructor (PROTECTED OR PUBLIC) — JPA require
    protected Project() {}

    // 2. Business constructor
    public Project(String name, String description, ProjectStatus status) {
        this.name = name;
        this.description = description;
        this.status = status;
        this.createdAt = Instant.now();
    }

    // 3. Getters
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getDescription() { return description; }
    public ProjectStatus getStatus() { return status; }
    public Instant getCreatedAt() { return createdAt; }

    // 4. Setters (chỉ cho field mutable nghiệp vụ)
    public void setName(String name) { this.name = name; }
    public void setDescription(String description) { this.description = description; }
    public void setStatus(ProjectStatus status) { this.status = status; }

    // 5. equals/hashCode based on ID
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Project p)) return false;
        return id != null && id.equals(p.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();   // constant — handle ID null/change
    }
}

5 requirement của JPA entity:

  1. @Entity annotation — đánh dấu class là entity.
  2. No-arg constructor (public hoặc protected) — Hibernate dùng reflection instantiate.
  3. Class không final — Hibernate sinh proxy CGLIB cho lazy loading.
  4. @Id field — primary key, không null.
  5. Mutable field với getter/setter — Hibernate set field qua setter.

Đây là lý do records không phải entity được: record là final + immutable + canonical constructor only. JPA spec require mutable POJO.

2. Vì sao records không work

// SAI — JPA reject
@Entity
public record Project(@Id Long id, String name) {}

3 issue:

  • record is final → Hibernate không subclass cho proxy.
  • Component không có setter → Hibernate không update field qua reflection.
  • Record không có no-arg constructor → reflection instantiate fail.

Workaround: dùng record cho DTO, class cho entity. Pattern Module 03 đã setup.

// Entity (DB)
@Entity public class Project { ... }

// DTO (API)
public record ProjectDto(Long id, String name, String description) {
    public static ProjectDto from(Project p) {
        return new ProjectDto(p.getId(), p.getName(), p.getDescription());
    }
}

Tách layer: DB entity + REST DTO. Project Module 03 đã làm — chỉ refactor entity từ record sang class.

3. @Id + @GeneratedValue strategies

@Id đánh dấu primary key. @GeneratedValue strategy quyết định cách sinh ID:

StrategyCơ chếUse caseDB support
IDENTITYAuto-increment column DBDefault Postgres/MySQLPostgres SERIAL/IDENTITY, MySQL AUTO_INCREMENT
SEQUENCEDB sequence objectHigh-throughput insert (batch)Postgres, Oracle, H2
TABLETracking table riêngPortable nhưng slowMọi DB
UUID (Hibernate 6+)UUID v4/v7 randomDistributed, no DB sequenceMọi DB
AUTOHibernate chọn theo dialectDefault — varying behavior

3.1 IDENTITY — phổ biến nhất

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Postgres SQL:

CREATE TABLE projects (
    id BIGSERIAL PRIMARY KEY,        -- auto-increment
    name VARCHAR(100) NOT NULL,
    ...
);

Hibernate generate INSERT, Postgres trả ID:

INSERT INTO projects (name, ...) VALUES (?, ...) RETURNING id;

Pros:

  • Đơn giản, no extra config.
  • ID tăng dần — sortable by creation order.

Cons:

  • Không batch insert được: Hibernate cần ID sau mỗi INSERT để track entity. IDENTITY column trả ID sau từng INSERT — không thể batch nhiều INSERT cùng lúc.
  • Limit 2^63 (BIGINT) — đủ cho 99% app.

3.2 SEQUENCE — high-throughput

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "project_seq")
@SequenceGenerator(name = "project_seq", sequenceName = "projects_id_seq", allocationSize = 50)
private Long id;

Postgres SQL:

CREATE SEQUENCE projects_id_seq INCREMENT 50 START 1;

CREATE TABLE projects (
    id BIGINT PRIMARY KEY,
    ...
);

Workflow:

  1. Hibernate query SELECT nextval('projects_id_seq') → trả 50 (block of 50 ID).
  2. Cache 50 ID local: 1, 2, ..., 50.
  3. INSERT 50 entity với ID local — không call DB sequence mỗi lần.
  4. Khi cạn → query nextval → 100 (next block).

Pros:

  • Batch insert work: Hibernate có ID trước INSERT, batch nhiều INSERT cùng lúc.
  • Performance high-throughput.

Cons:

  • Setup phức tạp hơn IDENTITY.
  • ID không strictly increasing (cluster of 50 có thể skip nếu app crash).

Khi nào chọn SEQUENCE: app insert >100 record/sec. Default for batch import job.

3.3 UUID — distributed system

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Hibernate 6+ support UUID native. Default UUID v4 (random). Hibernate 6.6+ support UUID v7 (time-ordered):

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@UuidGenerator(style = UuidGenerator.Style.TIME)   // UUID v7
private UUID id;

Pros:

  • Distributed-friendly: app sinh ID local, không depend DB. Microservice merge data dễ.
  • Security: ID không guessable — không expose record count.
  • No conflict giữa multiple DB (sharding, replication).

Cons:

  • Index bigger (16 bytes vs 8 bytes Long).
  • UUID v4 random → bad for B-tree index locality (insert cause page split).
  • UUID v7 fix problem locality nhưng cần Hibernate 6.6+.

Khi nào chọn UUID: distributed system, microservices, public-facing ID không expose count. TaskFlow Module 11 sẽ dùng UUID khi tách microservices.

4. @Column — column customization

@Column(
    name = "project_name",         // column name (default: snake_case từ field)
    nullable = false,                // NOT NULL constraint
    unique = true,                   // UNIQUE constraint
    length = 100,                    // VARCHAR(100)
    insertable = true,                // include in INSERT
    updatable = true,                 // include in UPDATE
    columnDefinition = "VARCHAR(100) CHECK (LENGTH(name) >= 3)"  // custom DDL
)
private String name;

Common pattern:

FieldAnnotation
Primary key@Id @GeneratedValue
Required field@Column(nullable = false)
Unique field@Column(unique = true) (also @Table(uniqueConstraints = ...))
String varchar@Column(length = 255) (default 255)
Long string@Column(columnDefinition = "TEXT")
Created timestamp@Column(updatable = false)
Soft delete flag@Column(name = "is_deleted")
Money@Column(precision = 19, scale = 4) (BigDecimal)

Lưu ý length: chỉ apply VARCHAR. Số (Integer, Long) ignore.

5. Naming strategy — camelCase ↔ snake_case

Hibernate có 2 naming strategy quyết định map field name → column name:

5.1 Implicit naming strategy

spring:
  jpa:
    hibernate:
      naming:
        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

SpringPhysicalNamingStrategy (Boot default):

  • firstNamefirst_name
  • XMLParserxml_parser
  • URLurl

Mapping snake_case Postgres convention. Đa phần dev expect.

Override per-field nếu cần:

@Column(name = "fname")          // explicit, không apply strategy
private String firstName;

5.2 Quy tắc thực tế

90% case dùng default SpringPhysicalNamingStrategy:

  • Java field camelCase → Postgres column snake_case.
  • Không cần @Column(name = ...) trừ khi tên đặc biệt.
@Entity
public class Project {
    @Id Long id;                    // → id
    String name;                     // → name
    String description;              // → description
    ProjectStatus status;            // → status
    Instant createdAt;               // → created_at (auto)
}

Verbose @Column mọi field = noise. Trust naming strategy.

6. @Enumerated — enum mapping

public enum ProjectStatus { PLANNING, ACTIVE, DONE, ARCHIVED }

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProjectStatus status;

2 mode:

ModeDB lưuProsCons
EnumType.STRING'ACTIVE' (varchar)Readable, refactor-safeBigger column
EnumType.ORDINAL1 (int)CompactReorder enum = data corruption

Quy tắc: luôn EnumType.STRING. ORDINAL danger:

public enum Status { PLANNING, ACTIVE, DONE }
// DB: 0, 1, 2

// Future: thêm enum value
public enum Status { PLANNING, INTERNAL_REVIEW, ACTIVE, DONE }
// DB: 0, 1, 2, 3
// Old row '1' = ACTIVE → bay gio thanh INTERNAL_REVIEW. CORRUPT DATA.

Không bao giờ ORDINAL. Cost negligible — readable + safe.

6.1 Custom enum mapping

public enum ProjectStatus {
    PLANNING("planning"),
    ACTIVE("active"),
    DONE("done"),
    ARCHIVED("archived");

    @Getter
    private final String code;

    ProjectStatus(String code) { this.code = code; }

    public static ProjectStatus from(String code) {
        for (ProjectStatus s : values()) {
            if (s.code.equals(code)) return s;
        }
        throw new IllegalArgumentException("Unknown: " + code);
    }
}

@Convert(converter = ProjectStatusConverter.class)
private ProjectStatus status;

@Converter
public class ProjectStatusConverter implements AttributeConverter<ProjectStatus, String> {
    public String convertToDatabaseColumn(ProjectStatus s) { return s == null ? null : s.getCode(); }
    public ProjectStatus convertToEntityAttribute(String code) { return code == null ? null : ProjectStatus.from(code); }
}

@Convert cho phép custom mapping cho field bất kỳ. Use case:

  • Enum có code khác name (legacy code).
  • Custom value object → string serialization.
  • Encryption (sensitive field).

7. @Embeddable — value object

DTO concepts như Money, Address, Email không phải entity (no ID, immutable). JPA support qua @Embeddable:

@Embeddable
public class Money {

    @Column(name = "amount_value", precision = 19, scale = 4)
    private BigDecimal amount;

    @Column(name = "amount_currency", length = 3)
    private String currency;

    protected Money() {}                               // JPA require
    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    // getters
}

@Entity
public class Order {
    @Id Long id;

    @Embedded
    private Money total;        // → 2 column: amount_value, amount_currency
}

DB schema:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    amount_value NUMERIC(19, 4),
    amount_currency VARCHAR(3)
);

@Embeddable value object stored inline trong table chủ — không phải table riêng.

7.1 Override column khi reuse

Reuse Money cho 2 field — override column:

@Entity
public class Order {
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "amount", column = @Column(name = "subtotal_value")),
        @AttributeOverride(name = "currency", column = @Column(name = "subtotal_currency"))
    })
    private Money subtotal;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "amount", column = @Column(name = "tax_value")),
        @AttributeOverride(name = "currency", column = @Column(name = "tax_currency"))
    })
    private Money tax;
}

Verbose nhưng explicit. Module 09 sẽ pattern this cho TaskFlow.

8. Lifecycle callbacks

Hibernate trigger callback methods at lifecycle events:

@Entity
public class Project {

    @PrePersist
    protected void onCreate() {
        if (createdAt == null) createdAt = Instant.now();
        if (status == null) status = ProjectStatus.PLANNING;
    }

    @PreUpdate
    protected void onUpdate() {
        // Note: KHONG set updatedAt qua method nay neu da co @LastModifiedDate
    }

    @PostLoad
    protected void afterLoad() {
        // Compute derived field tu data DB
    }

    @PreRemove
    protected void beforeDelete() {
        // Cleanup audit log
    }
}

7 callback có thể annotate: @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove, @PostLoad.

Use case phổ biến:

@PrePersist
protected void prePersist() {
    if (createdAt == null) createdAt = Instant.now();
}

@PreUpdate
protected void preUpdate() {
    updatedAt = Instant.now();
}

Hoặc dùng Spring Data Auditing — bài 03 sẽ giới thiệu (@CreatedDate, @LastModifiedDate).

9. Equals/hashCode — pitfall classic

JPA entity equals/hashCode là 1 trong bug tinh tế nhất Hibernate. Anti-pattern thường gặp:

// SAI 1: equals based on all fields (Lombok @EqualsAndHashCode)
@EqualsAndHashCode
@Entity
public class Project {
    @Id Long id;
    String name;
    Instant createdAt;
}

Vấn đề:

  • Hibernate proxy → field null trước khi load → equals fail.
  • Set/Map dùng entity làm key broken khi field change.
// SAI 2: equals based on ID, hashCode based on ID
@EqualsAndHashCode(of = "id")
@Entity
public class Project {
    @Id Long id;
}

Vấn đề:

  • Entity transient (chưa save): id == null. set.add(transient)hashCode = 0.
  • Sau persist: id = 42. set.contains(persistent)hashCode = 42 ≠ 0 → not found.

Đúng pattern (recommended):

@Entity
public class Project {
    @Id @GeneratedValue Long id;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Project p)) return false;
        return id != null && id.equals(p.id);    // null-id never equal
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();             // CONSTANT — same for all instance
    }
}

Logic:

  • equals: 2 entity equal nếu same class + ID không null + ID same.
  • hashCode: constant per class. Acceptable cho HashMap (lookup O(N) instead of O(1) — rare để store entity in Map).

hashCode constantacceptable trade-off vs hashCode = id.hashCode():

  • Constant → consistent throughout entity lifecycle (transient → managed).
  • Performance hit only when storing entity in HashMap (rare).

Reference: Vlad Mihalcea's blog "The best way to implement equals, hashCode in JPA".

10. @Table — table customization

@Entity
@Table(
    name = "projects",
    schema = "taskflow",
    indexes = {
        @Index(name = "idx_project_status", columnList = "status"),
        @Index(name = "idx_project_created", columnList = "created_at DESC")
    },
    uniqueConstraints = {
        @UniqueConstraint(name = "uk_project_name", columnNames = "name")
    }
)
public class Project { ... }

@Table define:

  • name: table name (default class name lowercase).
  • schema/catalog: DB schema.
  • indexes: index hint cho schema generation.
  • uniqueConstraints: composite unique.

Cảnh báo: Hibernate dùng @Table cho schema generation (ddl-auto). Production nên dùng Flyway migration thay vì rely vào Hibernate auto. Annotation chỉ là hint.

12. Vận hành production — equals/hashCode, large entity, audit trail

Entity design ảnh hưởng performance + correctness ở scale. Section này cover production gotcha.

12.1 equals/hashCode pattern an toàn

Pattern Vlad Mihalcea recommend:

@Entity
public class Project {
    @Id @GeneratedValue
    private Long id;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Project that)) return false;
        return id != null && id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();        // CONSTANT — independent of id
    }
}

3 quy tắc:

  1. equals based on ID only — null-safe.
  2. hashCode constant — entity chuyển từ transient (id null) sang persistent (id assigned) không thay đổi hashCode → vẫn tìm được trong HashSet.
  3. Không Lombok @Data — sinh equals/hashCode based on all fields → bug khi entity managed (lazy proxy, dirty checking).

Bug nếu hashCode dựa trên id mutable: thêm entity vào HashSet khi id null → save → id gán → hashCode đổi → set.contains(entity) trả false.

12.2 Large @Lob field — lazy load

Field @Lob String description (1MB+) load mỗi findById → slow.

@Entity
public class Project {
    @Lob
    @Basic(fetch = FetchType.LAZY)
    private String description;
}

Cảnh báo: @Basic LAZY cần bytecode enhancement (Hibernate 6+). Setup Maven plugin:

<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    <version>${hibernate.version}</version>
    <executions>
        <execution>
            <configuration>
                <enableLazyInitialization>true</enableLazyInitialization>
            </configuration>
            <goals><goal>enhance</goal></goals>
        </execution>
    </executions>
</plugin>

Hoặc tách table: Project core + ProjectContent 1-1 lazy → cleaner.

12.3 Audit trail enterprise — @MappedSuperclass

Pattern audit chuẩn:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
    @CreatedDate Instant createdAt;
    @LastModifiedDate Instant updatedAt;
    @CreatedBy String createdBy;
    @LastModifiedBy String updatedBy;
}

@Entity
public class Project extends Auditable { ... }

Cộng AuditorAware bean (lấy user từ Spring Security) → tự populate createdBy/updatedBy. Compliance + debug dễ.

12.4 Schema validation in CI

Production: bug entity không match DB schema khó phát hiện local. Add CI test với Testcontainers:

@SpringBootTest
@Testcontainers
class SchemaValidationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Test
    void hibernateValidatesSchema() {
        // ddl-auto: validate, Flyway migrate
        // Test fail nếu schema mismatch
    }
}

Run trong CI với DB real (Testcontainers) → catch missing migration trước deploy.

12.5 Failure runbook

Mode 1 — HibernateException: missing column:

  • Cause: entity field thêm nhưng Flyway migration miss.
  • Remediate: add migration, redeploy.

Mode 2 — Slow query khi entity load:

  • Diagnose: entity có nhiều @Lob hoặc collection EAGER.
  • Remediate: split entity, lazy load @Lob.

Mode 3 — OutOfMemoryError khi findAll:

  • Cause: load full entity với 1M row.
  • Remediate: pagination, DTO projection, streaming.

13. Pitfall tổng hợp

Nhầm 1: Dùng record cho entity. ✅ Class với getter/setter + no-arg constructor. Record cho DTO.

Nhầm 2: EnumType.ORDINAL.

@Enumerated(EnumType.ORDINAL)     // mang nguy hiem

✅ Always STRING. Reorder enum không corrupt data.

Nhầm 3: Lombok @Data cho entity.

@Data       // sinh equals/hashCode based on all fields
@Entity
public class Project { ... }

✅ Manual equals/hashCode based on ID null-safe + constant hashCode. Hoặc Lombok @EqualsAndHashCode(onlyExplicitlyIncluded = true) + manual.

Nhầm 4: Class entity final.

@Entity
public final class Project { ... }    // Hibernate khong tao proxy — fail

✅ Bỏ final. Hibernate cần subclass cho lazy loading proxy.

Nhầm 5: Quên no-arg constructor.

@Entity
public class Project {
    @Id Long id;
    public Project(Long id) { ... }     // chi co arg constructor — JPA fail
}

✅ Add protected Project() {} (no-arg). Hibernate dùng reflection instantiate.

Nhầm 6: Rely vào ddl-auto cho schema production. ✅ validate mode + Flyway migration. Bài 06 đào sâu.

Nhầm 7: Setter cho ID.

public void setId(Long id) { this.id = id; }    // ID khong nen mutable

✅ Bỏ setter ID. Hibernate set qua reflection trực tiếp field.

13. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

14. Tóm tắt

  • Entity = mutable Java class với @Entity + no-arg constructor + getter/setter + @Id.
  • Records không phải entity — final + immutable. Dùng cho DTO. Class cho entity.
  • @Id strategies: IDENTITY (default), SEQUENCE (high-throughput), UUID (distributed). AUTO không recommend.
  • IDENTITY không batch insert được. SEQUENCE work với batch.
  • UUID v7 (Hibernate 6.6+) time-ordered, locality tốt cho B-tree index.
  • @Column customize column name, nullable, unique, length.
  • Naming strategy Spring Boot default: SpringPhysicalNamingStrategy — camelCase → snake_case auto.
  • @Enumerated(EnumType.STRING) always. ORDINAL gây data corruption khi reorder enum.
  • @Embeddable cho value object stored inline với entity chủ.
  • Lifecycle callback @PrePersist/@PreUpdate/@PostLoad — set timestamp, default value, derived field.
  • equals/hashCode pattern: equals based on ID null-safe, hashCode constant per class. Avoid Lombok @Data/@EqualsAndHashCode.
  • @Table với indexes, uniqueConstraints — hint schema generation. Production dùng Flyway thay vì rely.

15. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao Java records không phải entity được? Hibernate yêu cầu cụ thể gì?

3 lý do records không work:

  1. Records là final: JLS spec define record class implicitly final. Hibernate cần subclass để tạo proxy CGLIB cho lazy loading. Final class → cannot subclass → cannot proxy → lazy load fail.
  2. Records không có no-arg constructor: records chỉ có canonical constructor (với tất cả component). Hibernate dùng reflection instantiate empty entity rồi set field — cần no-arg constructor mặc định.
  3. Records component immutable: không setter. Hibernate update field qua reflection (set field private) — ok về mặt kỹ thuật, nhưng JPA spec không define behavior cho immutable entity.

Hibernate yêu cầu chính thức:

  • @Entity annotation.
  • Class non-final (cho proxy).
  • No-arg constructor public hoặc protected.
  • Field non-final hoặc mutable qua setter.
  • @Id field unique.

Pattern recommended 2026:

// DB layer — class entity
@Entity
@Table(name = "projects")
public class Project {
  @Id @GeneratedValue
  private Long id;

  @Column(nullable = false)
  private String name;

  protected Project() {}
  public Project(String name) { this.name = name; }
  // getters, setters
}

// API layer — record DTO
public record ProjectDto(Long id, String name) {
  public static ProjectDto from(Project p) {
      return new ProjectDto(p.getId(), p.getName());
  }
}

Lợi ích tách layer:

  • Entity tự do refactor — không lộ ra API.
  • DTO có shape khác entity (subset field, computed field).
  • API versioning: ProjectDtoV1, ProjectDtoV2 không động entity.

Note: Spring Data JDBC (khác Spring Data JPA) support records — vì JDBC không có lazy loading proxy. Module 09 sẽ giới thiệu khi nào pick JDBC over JPA.

Q2
So sánh IDENTITY vs SEQUENCE strategy. Vì sao IDENTITY không batch insert được?
AspectIDENTITYSEQUENCE
Setup@GeneratedValue(IDENTITY)@GeneratedValue(SEQUENCE) + @SequenceGenerator
DB DDLid BIGSERIALCREATE SEQUENCE + id BIGINT
ID assignmentAfter INSERTBefore INSERT (from sequence)
Batch insert❌ Không work✅ Work
ID monotonicStrict (1, 2, 3, ...)Cluster (1, 2, ..., 50, 101, 102, ..., 150)
PerformanceLower (1 round trip per INSERT)Higher (1 sequence call per N insert)
Postgres supportNative (BIGSERIAL)Native (CREATE SEQUENCE)
Default Spring Data✅ DefaultManual setup

Vì sao IDENTITY không batch:

Workflow batch insert (5 entity):

// Hibernate goal:
INSERT INTO projects (name, ...) VALUES (?, ...), (?, ...), (?, ...), (?, ...), (?, ...);
// 1 SQL — 5 row, 1 round trip

Vấn đề với IDENTITY:

  • Hibernate cần ID sau INSERT để track entity trong persistence context.
  • IDENTITY column trả ID **per-row** sau INSERT (qua RETURNING id Postgres hoặc generated_keys JDBC).
  • Batch INSERT không thể return per-row ID đúng cách qua single statement.
  • Hibernate fall back: 1 INSERT statement per entity → 5 round trip cho 5 entity.

Workflow SEQUENCE batch:

// Step 1: pre-allocate IDs
SELECT nextval('projects_id_seq');     -- returns 100
SELECT nextval('projects_id_seq');     -- returns 200 (allocationSize=100)
// → Hibernate cache IDs 100-199 locally

// Step 2: assign ID before INSERT (no DB call)
projects[0].id = 100
projects[1].id = 101
projects[2].id = 102

// Step 3: BATCH INSERT
INSERT INTO projects (id, name, ...) VALUES (100, ?), (101, ?), (102, ?);
// 1 round trip for all 3

Performance khác biệt:

  • Insert 1000 record:
    • IDENTITY: 1000 round trip → ~1-2s (50ms latency / 1Gbps).
    • SEQUENCE (allocationSize=100): 10 sequence call + 10 batch INSERT → ~50ms.

Khi nào chọn:

  • IDENTITY: app standard CRUD, low insert rate (<100/sec). Default cho 90% case.
  • SEQUENCE: bulk import, batch processing (CSV, ETL), insert rate cao. Đáng setup overhead.

Bonus enable batch trong Hibernate:

spring.jpa.properties.hibernate.jdbc.batch_size: 50
spring.jpa.properties.hibernate.order_inserts: true
spring.jpa.properties.hibernate.order_updates: true

Module 09 (Performance) sẽ đào sâu batch optimization.

Q3
Đoạn entity sau có 4 vấn đề. Liệt kê + sửa.
@Entity
@Data        // Lombok
public final class Project {

  @Id
  @GeneratedValue
  public Long id;

  public String name;

  @Enumerated(EnumType.ORDINAL)
  public ProjectStatus status;

  public Project(String name) {
      this.name = name;
  }
}
  1. Class final: Hibernate cần subclass cho proxy. final → fail tại runtime "could not generate enhancer".
    // Bo final
    @Entity
    public class Project { ... }
  2. Quên no-arg constructor: JPA require. Hibernate try instantiate qua reflection → fail.
    protected Project() {}                  // no-arg
    public Project(String name) { ... }     // business
  3. @Data Lombok: sinh equals/hashCode based on all fields. Bug:
    • Lazy proxy → field null → equals fail.
    • Mutable field change → hashCode change → broken trong HashSet.
    • Performance: equals iterate all fields.
    // Thay @Data bang manual hoac selective Lombok
    @Getter @Setter           // OK
    @ToString(of = "id")       // exclude lazy field
    public class Project {
    
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (!(o instanceof Project p)) return false;
          return id != null && id.equals(p.id);
      }
    
      @Override
      public int hashCode() {
          return getClass().hashCode();
      }
    }
  4. EnumType.ORDINAL: reorder enum value → data corruption.
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private ProjectStatus status;

Bonus issues:

  • public field thay private + getter/setter — phá encapsulation. Hibernate detect property qua getter, không qua field public.
  • @GeneratedValue không strategy — fall back AUTO, behavior tuỳ Hibernate version. Explicit IDENTITY hoặc SEQUENCE.

Code đúng đầy đủ:

@Entity
@Table(name = "projects")
@Getter @Setter
public class Project {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, unique = true, length = 100)
  private String name;

  @Enumerated(EnumType.STRING)
  @Column(nullable = false, length = 20)
  private ProjectStatus status;

  @Column(name = "created_at", nullable = false, updatable = false)
  private Instant createdAt = Instant.now();

  protected Project() {}

  public Project(String name, ProjectStatus status) {
      this.name = name;
      this.status = status;
  }

  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Project p)) return false;
      return id != null && id.equals(p.id);
  }

  @Override
  public int hashCode() {
      return getClass().hashCode();
  }
}
Q4
Project có field Money total (BigDecimal amount + String currency). Khi nào dùng @Embeddable vs entity riêng?

Quy tắc: identity matters? → entity. Value-only? → @Embeddable.

Aspect@Embeddable (Money)Entity (PaymentMethod)
IdentityKhông có IDCó @Id
LifecycleCùng entity chủĐộc lập (CRUD riêng)
StorageInline column trong table chủTable riêng
ReuseCopy valueReference qua FK
EqualityBy value (all field)By ID
ModificationReplace whole objectUpdate through tx
Use caseMoney, Address, Email, CoordinateUser, Order, Project, PaymentMethod

Money là @Embeddable:

@Embeddable
public class Money {
  @Column(name = "amount_value", precision = 19, scale = 4)
  private BigDecimal amount;

  @Column(name = "amount_currency", length = 3)
  private String currency;

  protected Money() {}
  public Money(BigDecimal amount, String currency) { ... }

  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Money m)) return false;
      return amount.equals(m.amount) && currency.equals(m.currency);
  }

  @Override
  public int hashCode() { return Objects.hash(amount, currency); }
}

@Entity
public class Order {
  @Id Long id;

  @Embedded
  private Money total;        // → 2 column trong table orders

  @Embedded
  @AttributeOverrides({
      @AttributeOverride(name = "amount", column = @Column(name = "tax_value")),
      @AttributeOverride(name = "currency", column = @Column(name = "tax_currency"))
  })
  private Money tax;          // → 2 column khac voi prefix tax_
}

DB schema:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  amount_value NUMERIC(19, 4),
  amount_currency VARCHAR(3),
  tax_value NUMERIC(19, 4),
  tax_currency VARCHAR(3)
);

Khi nào convert Money sang entity?

  • Cần tracking history: "This order's tax used to be $5, now $7" — cần audit table riêng.
  • Cần reference shared: 100 Order cùng có giá 99.99 USD — share 1 PriceTag entity (rare, premature optimization).
  • Cần CRUD độc lập: admin endpoint update Money mà không động Order — almost never makes sense for value.

Quy tắc: 99% case Money/Address/Email là @Embeddable. Entity overkill cho value.

Modern alternatives:

  • JSON column: store Money as JSON trong DB (Postgres JSONB). Simpler than @Embeddable cho schema thay đổi nhiều. Nhưng query khó hơn (JSON path operator).
  • Custom @Convert: serialize Money → "USD 99.99" string. Nhỏ gọn nhưng lose query (query WHERE amount > X impossible).

@Embeddable là choice mainstream — best balance.

Q5
Đoạn equals/hashCode sau có 2 anti-pattern. Cách đúng?
@Entity
public class Project {
  @Id @GeneratedValue
  private Long id;

  @Column private String name;

  @Override
  public boolean equals(Object o) {
      if (!(o instanceof Project)) return false;
      Project p = (Project) o;
      return id.equals(p.id) && name.equals(p.name);     // (1)
  }

  @Override
  public int hashCode() {
      return Objects.hash(id, name);                      // (2)
  }
}

Vấn đề:

  1. Equals dùng all fields:
    • Lazy proxy: name chưa load → null → NPE trong name.equals(...).
    • Hai entity với cùng ID nhưng name khác → không equal? Sai semantic — same DB row.
    • Performance: load all fields chỉ để equals.
  2. HashCode mutable based on field:
    • Entity transient: id == null. Add to HashSet → hashCode = Objects.hash(null, name).
    • After persist: id = 42. set.contains() → hashCode change → not found.
    • Mutable name change → hashCode change → broken HashSet/HashMap.

Đúng pattern:

@Entity
public class Project {
  @Id @GeneratedValue
  private Long id;

  @Column private String name;

  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Project p)) return false;
      return id != null && id.equals(p.id);    // null-id NEVER equal
  }

  @Override
  public int hashCode() {
      return getClass().hashCode();             // CONSTANT
  }
}

Logic:

  • Equals null-safe: nếu id == null, return false. 2 entity transient khác nhau (chưa save) — equal hoặc không không meaningful.
  • HashCode constant: mọi instance Project có cùng hashCode. Trade-off:
    • Pros: stable suốt entity lifecycle (transient → managed). Set/Map work consistent.
    • Cons: HashMap/HashSet collision rate = 100%. Lookup degraded O(N) thay O(1).
  • Practical impact: rarely store entity in HashMap as key. Performance hit irrelevant for 99% use case.

Alternative — `@NaturalId`:

@Entity
public class Project {
  @Id @GeneratedValue
  private Long id;

  @NaturalId
  @Column(nullable = false, unique = true)
  private String slug;     // business identifier, immutable

  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Project p)) return false;
      return slug.equals(p.slug);    // immutable identifier
  }

  @Override
  public int hashCode() {
      return slug.hashCode();
  }
}

Nếu entity có **business identifier** unique + immutable (vd `slug`, `email`, `sku`), dùng nó cho equals/hashCode. Stable across lifecycle.

Reference: Vlad Mihalcea blog "The best way to implement equals, hashCode in JPA" — bible cho pattern này.

Q6
Spring Boot naming strategy `SpringPhysicalNamingStrategy` map `firstName` → `first_name`. Test trên app multi-database (Postgres + Oracle): `xmlData` → tên column nào? Có conflict với reserved keyword không?

Naming strategy mapping:

firstName    → first_name      (camelCase → snake_case)
xmlData      → xml_data         (lowercase + underscore)
XMLParser    → xml_parser
URL          → url
TaskFlow     → task_flow
isActive     → is_active        (boolean prefix preserved)
ABCdef       → ab_cdef          (boundary detection)

Conflict reserved keyword:

SQL có nhiều reserved keyword: USER, ORDER, TABLE, SELECT, FROM, WHERE, ... Naming strategy default không quote identifier.

Vấn đề:

@Entity
public class Order {           // → table "order" - conflict SQL keyword
  @Id Long id;
  String user;                // → column "user" - conflict
}

// Hibernate generate:
CREATE TABLE order (...)        -- Postgres syntax error
INSERT INTO order ...            -- Postgres syntax error

3 cách fix:

  1. Đổi tên entity tránh keyword:
    @Entity
    @Table(name = "customer_orders")
    public class Order {
      @Column(name = "user_id")
      Long userId;
    }
    Pragmatic, recommended.
  2. Quote identifier:
    @Entity
    @Table(name = "\"order\"")     // double-quote
    public class Order {
      @Column(name = "\"user\"")
      String user;
    }
    Hibernate generate "order" với double-quote — Postgres accept. Nhưng raw SQL queries phải always quote — pain.
  3. Global setting auto-quote keywords:
    spring.jpa.properties.hibernate.globally_quoted_identifiers: true
    spring.jpa.properties.hibernate.auto_quote_keyword: true
    Hibernate detect keyword + quote auto. Slow startup (load keyword list).

Multi-database concerns:

  • Postgres lowercase by default: first_name map naturally.
  • Oracle uppercase by default: Hibernate quote unquoted identifier hoặc convert UPPER. Dialect handle.
  • MySQL backtick: `order`. Different dialect.

Hibernate dialect (chọn theo spring.jpa.database-platform) handle quoting per DB. Naming strategy generates **logical name**, dialect convert to **physical name** đúng convention DB.

Best practice 2026:

  • Tránh entity/field name conflict SQL keyword. List: Postgres reserved words.
  • Default Spring Boot naming strategy đủ cho 95% case.
  • Override per field qua @Column(name = ...) nếu cần legacy compatibility.
  • Multi-DB app — test với mỗi DB target. Dialect handle phần lớn.

Bài tiếp theo: Repository abstraction — JpaRepository, derived queries, @Query

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...