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:
@Entityannotation — đánh dấu class là entity.- No-arg constructor (public hoặc protected) — Hibernate dùng reflection instantiate.
- Class không
final— Hibernate sinh proxy CGLIB cho lazy loading. @Idfield — primary key, không null.- 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:
recordis 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:
| Strategy | Cơ chế | Use case | DB support |
|---|---|---|---|
IDENTITY | Auto-increment column DB | Default Postgres/MySQL | Postgres SERIAL/IDENTITY, MySQL AUTO_INCREMENT |
SEQUENCE | DB sequence object | High-throughput insert (batch) | Postgres, Oracle, H2 |
TABLE | Tracking table riêng | Portable nhưng slow | Mọi DB |
UUID (Hibernate 6+) | UUID v4/v7 random | Distributed, no DB sequence | Mọi DB |
AUTO | Hibernate chọn theo dialect | Default — 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:
- Hibernate query
SELECT nextval('projects_id_seq')→ trả 50 (block of 50 ID). - Cache 50 ID local: 1, 2, ..., 50.
- INSERT 50 entity với ID local — không call DB sequence mỗi lần.
- 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:
| Field | Annotation |
|---|---|
| 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):
firstName→first_nameXMLParser→xml_parserURL→url
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:
| Mode | DB lưu | Pros | Cons |
|---|---|---|---|
EnumType.STRING | 'ACTIVE' (varchar) | Readable, refactor-safe | Bigger column |
EnumType.ORDINAL | 1 (int) | Compact | Reorder 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 constant là acceptable 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:
equalsbased on ID only — null-safe.hashCodeconstant — entity chuyển từ transient (id null) sang persistent (id assigned) không thay đổi hashCode → vẫn tìm được trongHashSet.- 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
@Lobhoặ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
Hibernate:
- Hibernate User Guide — Mapping Entities
- Hibernate User Guide — Generated Values
- Hibernate User Guide — Naming Strategy
JPA spec:
Spring:
Vlad Mihalcea blogs:
- The best way to implement equals, hashCode in JPA
- The best way to map an enum with Hibernate
- SEQUENCE vs IDENTITY
Tool:
- IntelliJ "Persistence" tool window — visualize entity diagram + verify mapping.
- Hibernate Tools (
hbm2ddl) — preview SQL DDL từ entity.
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.
@Idstrategies: 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.
@Columncustomize 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.@Embeddablecho 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. @Tablevới indexes, uniqueConstraints — hint schema generation. Production dùng Flyway thay vì rely.
15. Tự kiểm tra
Q1Vì sao Java records không phải entity được? Hibernate yêu cầu cụ thể gì?▸
3 lý do records không work:
- 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.
- 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.
- 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:
@Entityannotation.- Class non-final (cho proxy).
- No-arg constructor public hoặc protected.
- Field non-final hoặc mutable qua setter.
@Idfield 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,ProjectDtoV2khô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.
Q2So sánh IDENTITY vs SEQUENCE strategy. Vì sao IDENTITY không batch insert được?▸
| Aspect | IDENTITY | SEQUENCE |
|---|---|---|
| Setup | @GeneratedValue(IDENTITY) | @GeneratedValue(SEQUENCE) + @SequenceGenerator |
| DB DDL | id BIGSERIAL | CREATE SEQUENCE + id BIGINT |
| ID assignment | After INSERT | Before INSERT (from sequence) |
| Batch insert | ❌ Không work | ✅ Work |
| ID monotonic | Strict (1, 2, 3, ...) | Cluster (1, 2, ..., 50, 101, 102, ..., 150) |
| Performance | Lower (1 round trip per INSERT) | Higher (1 sequence call per N insert) |
| Postgres support | Native (BIGSERIAL) | Native (CREATE SEQUENCE) |
| Default Spring Data | ✅ Default | Manual 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 tripVấ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 idPostgres hoặcgenerated_keysJDBC). - 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 3Performance 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: trueModule 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;
}
}
▸
@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;
}
}- Class final: Hibernate cần subclass cho proxy.
final→ fail tại runtime "could not generate enhancer".// Bo final @Entity public class Project { ... } - Quên no-arg constructor: JPA require. Hibernate try instantiate qua reflection → fail.
protected Project() {} // no-arg public Project(String name) { ... } // business @DataLombok: 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(); } }EnumType.ORDINAL: reorder enum value → data corruption.@Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private ProjectStatus status;
Bonus issues:
publicfield thay private + getter/setter — phá encapsulation. Hibernate detect property qua getter, không qua field public.@GeneratedValuekhô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();
}
}Q4Project có field Money total (BigDecimal amount + String currency). Khi nào dùng @Embeddable vs entity riêng?▸
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) |
|---|---|---|
| Identity | Không có ID | Có @Id |
| Lifecycle | Cùng entity chủ | Độc lập (CRUD riêng) |
| Storage | Inline column trong table chủ | Table riêng |
| Reuse | Copy value | Reference qua FK |
| Equality | By value (all field) | By ID |
| Modification | Replace whole object | Update through tx |
| Use case | Money, Address, Email, Coordinate | User, 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 > Ximpossible).
@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)
}
}
▸
@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 đề:
- Equals dùng all fields:
- Lazy proxy:
namechưa load → null → NPE trongname.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.
- Lazy proxy:
- 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.
- Entity transient:
Đú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.
Q6Spring 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 error3 cách fix:
- Đổi tên entity tránh keyword:Pragmatic, recommended.
@Entity @Table(name = "customer_orders") public class Order { @Column(name = "user_id") Long userId; } - Quote identifier:Hibernate generate
@Entity @Table(name = "\"order\"") // double-quote public class Order { @Column(name = "\"user\"") String user; }"order"với double-quote — Postgres accept. Nhưng raw SQL queries phải always quote — pain. - Global setting auto-quote keywords:Hibernate detect keyword + quote auto. Slow startup (load keyword list).
spring.jpa.properties.hibernate.globally_quoted_identifiers: true spring.jpa.properties.hibernate.auto_quote_keyword: true
Multi-database concerns:
- Postgres lowercase by default:
first_namemap 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...