EntityManager, JPQL & Spring Data — từ boilerplate tới interface thuần túy
EntityManager là core API của JPA quản lý vòng đời entity và persistence context. JPQL query entity thay vì table. Spring Data JPA sinh repository implementation tại runtime để bỏ hết boilerplate EntityManager. ddl-auto quyết định ai kiểm soát schema.
TL;DR: EntityManager là API trung tâm của JPA — bốn method cốt lõi persist/find/merge/remove thao tác entity; phía sau chúng là persistence context (first-level cache + dirty tracking) hoạt động trong phạm vi một transaction. JPQL query entity class thay vì table nên portable qua DB. Spring Data JPA đi xa hơn — bạn chỉ khai báo interface, framework sinh proxy implementation tại runtime từ method name, loại bỏ hoàn toàn boilerplate EntityManager. ddl-auto kiểm soát ai được phép sửa schema: validate an toàn cho production, update chỉ dùng ở dev, none khi migration tool (Flyway/Liquibase) làm chủ.
Bài trước giải thích vì sao ORM tồn tại và 5 impedance mismatch giữa Java object model với SQL relational model. Bài này đi tiếp: cụ thể bạn dùng API gì để thao tác entity, và tại sao Spring Data giúp bạn không phải dùng API đó trực tiếp.
1. EntityManager — giao diện trung tâm JPA
EntityManager (EM) là interface JPA dùng để thao tác entity — tương tự Connection trong JDBC nhưng làm việc ở object world thay vì SQL world. Mỗi EntityManager instance quản lý một persistence context riêng.
Bốn method thao tác entity cốt lõi:
@PersistenceContext
private EntityManager em;
// 1. persist — chuyển Transient → Managed, INSERT lúc commit
Project project = new Project("Mobile App", ProjectStatus.PLANNING);
em.persist(project);
// Chua co SQL ngay bay gio. SQL INSERT chay luc transaction commit.
// 2. find — SELECT by PK, tra ve Managed entity
Project loaded = em.find(Project.class, 42L);
// SQL: SELECT * FROM projects WHERE id = 42
// 3. merge — Detached → Managed, UPDATE luc commit
loaded.setStatus(ProjectStatus.ACTIVE);
Project managed = em.merge(loaded);
// managed la instance duoc em tracking tu day
// 4. remove — danh dau DELETE, thuc thi luc commit
em.remove(managed);
// SQL DELETE chay luc commit
Điểm quan trọng: persist, merge, remove không chạy SQL ngay lập tức — thay đổi được dồn lại đến thời điểm flush (mục 1.3 bên dưới).
1.1 Persistence context — first-level cache
Persistence context là Map từ entity identity (class + PK) sang entity instance mà EntityManager duy trì trong phạm vi một transaction. Nó cung cấp hai đảm bảo quan trọng:
Identity guarantee — trong cùng một transaction, mọi find với cùng PK trả về cùng một instance:
@Transactional
public void identityExample() {
Project p1 = repo.findById(42L).orElseThrow(); // SELECT
Project p2 = repo.findById(42L).orElseThrow(); // Cache hit — KHONG SELECT
System.out.println(p1 == p2); // true
}
Lần gọi thứ hai không sinh SQL vì Hibernate tìm thấy entity trong persistence context. Đây khác với L2 cache (cross-transaction, optional) — L1 cache luôn bật, không cấu hình được.
Dirty checking — Hibernate chụp snapshot entity lúc load. Khi transaction commit, nó so sánh state hiện tại với snapshot; field nào thay đổi sẽ được sinh UPDATE tự động:
@Transactional
public void dirtyCheckExample(Long id, String newName) {
Project p = repo.findById(id).orElseThrow();
p.setName(newName); // KHONG goi save()
// Method return → tx commit → Hibernate detect dirty → auto UPDATE
}
Bạn không cần gọi save() hay em.update(). Hibernate tự theo dõi mọi thay đổi trên Managed entity.
1.2 Entity lifecycle — bốn trạng thái
stateDiagram-v2
[*] --> Transient: new Project()
Transient --> Managed: em.persist()
Managed --> Detached: em.close() / tx end
Detached --> Managed: em.merge()
Managed --> Removed: em.remove()
Removed --> [*]: tx commit| Trạng thái | Ý nghĩa |
|---|---|
| Transient | Object mới tạo, EM không biết đến. new Project(). |
| Managed | EM đang tracking. Change tự sync DB. Sau persist()/find()/merge(). |
| Detached | EM đã đóng hoặc detach() gọi. Change không sync nữa. |
| Removed | em.remove() đã gọi. DELETE sẽ chạy lúc commit. |
Phần lớn bug "tại sao change không save" xảy ra ở trạng thái Detached — entity ra ngoài transaction boundary, dirty checking không còn hoạt động. Fix chuẩn: annotate service method @Transactional để toàn bộ xử lý nằm trong một transaction.
public Project load(Long id) {
// KHONG @Transactional — tx cua repo.findById ket thuc ngay
Project p = repo.findById(id).orElseThrow();
p.setName("New name"); // Detached — KHONG sync DB
return p;
}
Sửa đúng: thêm @Transactional vào method, hoặc gọi repo.save(p) sau khi modify.
1.3 Flush timing — khi nào Hibernate thực sự phát SQL
Mọi thay đổi trên managed entity được dồn lại (deferred write) và chỉ thành SQL tại đúng 3 thời điểm:
- Trước khi transaction commit — flush cuối, phổ biến nhất.
- Trước một query liên quan (flush mode
AUTOmặc định): bạnpersist(task)rồi chạy JPQLSELECT ... FROM Tasktrong cùng transaction — Hibernate flush trước để query không đọc thiếu data vừa ghi. - Khi gọi
em.flush()thủ công — khi cần ID hoặc muốn constraint violation lộ ra ngay.
stateDiagram-v2
[*] --> Transient: new Entity()
Transient --> Managed: persist()
Managed --> Dirty: setter doi field
Dirty --> Flushed: truoc query lien quan / truoc commit / flush() thu cong
Flushed --> Detached: tx end / em.close()Deferred write cho phép Hibernate gom nhiều thay đổi vào một lần flush thay vì round-trip riêng lẻ. Nó cũng giải thích hiện tượng "UPDATE chạy dù tôi không gọi save()": tới thời điểm flush, dirty checking quét mọi managed entity và phát SQL cho bất kỳ entity nào lệch snapshot.
2. JPQL — query entity, không query table
JPQL (Jakarta Persistence Query Language) là ngôn ngữ query của JPA. Cú pháp giống SQL nhưng điểm khác biệt cốt lõi: JPQL query entity class và field, không query table và column.
// JPQL — dung ten entity class va field
List<Project> result = em.createQuery(
"SELECT p FROM Project p WHERE p.status = :status AND p.createdAt > :since",
Project.class
).setParameter("status", ProjectStatus.ACTIVE)
.setParameter("since", cutoff)
.getResultList();
Hibernate compile JPQL thành SQL theo dialect của DB đang dùng:
-- SQL Hibernate sinh ra (PostgreSQL dialect)
SELECT p.id, p.name, p.description, p.status, p.created_at
FROM projects p
WHERE p.status = 'ACTIVE' AND p.created_at > ?
Mapping p.status → p.status (column), p.createdAt → p.created_at (naming strategy camelCase → snake_case) do Hibernate xử lý tự động theo @Column annotation hoặc naming strategy mặc định.
2.1 Vì sao JPQL thay vì SQL trực tiếp
Lý do JPQL tồn tại thay vì viết SQL thẳng:
Portable qua DB: JPQL không biết dialect. Hibernate dịch sang SQL đúng cho PostgreSQL, MySQL, Oracle, H2. Đổi DB chỉ cần đổi config, không sửa query.
Object navigation: JPQL cho phép navigate association trực tiếp:
SELECT p FROM Project p WHERE p.customer.region = :region
Hibernate tự sinh JOIN cần thiết. Với SQL bạn phải viết JOIN tay và biết tên bảng.
Type safe hơn SQL string: Entity field tên sai → fail tại startup khi Spring Data parse, không phải lúc runtime.
Tuy nhiên JPQL không thay thế SQL — khi cần tính năng DB-specific (Postgres JSONB, window function, full-text search), dùng nativeQuery = true:
@Query(value = "SELECT * FROM projects WHERE created_at > ?1", nativeQuery = true)
List<Project> findRecentNative(Instant since);
90% case JPQL đủ. Drop xuống native SQL khi gặp giới hạn cụ thể.
3. Cơ chế bên dưới — Spring Data sinh repository implementation
Spring Data JPA là lý do bạn hiếm khi phải viết em.createQuery(...) trực tiếp. Bạn chỉ khai báo interface:
public interface ProjectRepository extends JpaRepository<Project, Long> {
List<Project> findByStatus(ProjectStatus status);
Optional<Project> findByNameIgnoreCase(String name);
long countByStatus(ProjectStatus status);
}
Không có implementation class nào. Vậy Spring làm gì để interface này hoạt động?
flowchart TB
Boot["Spring Boot startup"]
Scanner["JpaRepositoriesAutoConfiguration<br/>scan @Repository interfaces"]
Factory["RepositoryFactoryBean<br/>cho tung interface"]
Proxy["JDK Dynamic Proxy<br/>ProjectRepository$$Proxy"]
Parser["PartTreeJpaQuery<br/>parse method name -> JPQL"]
Simple["SimpleJpaRepository<br/>built-in: save findById delete..."]
EM["EntityManager"]
DB[("PostgreSQL")]
Boot --> Scanner
Scanner --> Factory
Factory --> Proxy
Proxy --> Simple
Proxy --> Parser
Parser -->|"JPQL template"| EM
Simple -->|"entity operations"| EM
EM --> DBQuá trình diễn ra tại startup, trước khi nhận request đầu tiên:
JpaRepositoriesAutoConfigurationscan package, tìm mọi interface extendsJpaRepository.- Với mỗi interface,
RepositoryFactoryBeantạo một JDK dynamic proxy implement interface đó. - Proxy có hai đường xử lý:
- Method built-in (
save,findById,delete,findAll,...): delegate thẳng xuốngSimpleJpaRepository— đây là implementation mặc định có sẵn trong Spring Data. - Method derived (
findByStatus,countByStatus,...):PartTreeJpaQueryparse tên method tại startup, build JPQL template, cache lại. Mỗi lần gọi chỉ bind parameter và execute.
- Method built-in (
- Proxy được đăng ký như Spring bean thông thường.
3.1 Method name parsing grammar
PartTreeJpaQuery parse method name theo grammar:
[verb][subject]By[property][keyword][And/Or][property][keyword]...
Ví dụ:
| Method name | JPQL tương đương |
|---|---|
findByStatus(Status s) | WHERE p.status = ?1 |
findByStatusIn(List<Status> s) | WHERE p.status IN ?1 |
findByNameContainingIgnoreCase(String n) | WHERE LOWER(p.name) LIKE LOWER(?) |
findByCreatedAtBetween(Instant s, Instant e) | WHERE p.createdAt BETWEEN ?1 AND ?2 |
findTop5ByStatusOrderByCreatedAtDesc(Status s) | WHERE p.status = ?1 ORDER BY p.created_at DESC LIMIT 5 |
countByStatus(Status s) | SELECT COUNT(p) WHERE p.status = ?1 |
existsByName(String n) | SELECT CASE WHEN COUNT(p) > 0 THEN TRUE ELSE FALSE END WHERE p.name = ?1 |
Property không tồn tại trong entity → fail tại startup với PropertyReferenceException: No property 'custmer' found for type 'Project'. Đây là fail-fast có chủ đích — bug phát hiện lúc deploy, không phải lúc user gọi đúng endpoint đó lần đầu.
3.2 Vì sao Spring Data thay vì gọi EntityManager trực tiếp
Trước Spring Data, code JPA thuần trông như thế này:
@Repository
public class ProjectRepositoryImpl {
@PersistenceContext
private EntityManager em;
public List<Project> findByStatus(ProjectStatus status) {
return em.createQuery(
"SELECT p FROM Project p WHERE p.status = :status",
Project.class
).setParameter("status", status).getResultList();
}
public Optional<Project> findById(Long id) {
return Optional.ofNullable(em.find(Project.class, id));
}
public Project save(Project p) {
if (p.getId() == null) {
em.persist(p);
return p;
}
return em.merge(p);
}
// ... tiep tuc cho delete, count, exists ...
}
Mỗi method là một đoạn boilerplate lặp cấu trúc giống nhau. App có 10 entity → 10 class kiểu này, mỗi cái dài 50-100 dòng.
Spring Data loại bỏ toàn bộ phần này. Interface ProjectRepository extends JpaRepository<Project, Long> khai báo hợp đồng; Spring sinh thực thi tại runtime. Developer chỉ cần thêm method khi hợp đồng cần mở rộng.
Khi derived query không đủ biểu đạt (logic phức tạp, JOIN nhiều bảng, aggregate), dùng @Query:
@Query("SELECT p FROM Project p LEFT JOIN FETCH p.tasks WHERE p.id = :id")
Optional<Project> findByIdWithTasks(@Param("id") Long id);
@Query("SELECT p.status, COUNT(p) FROM Project p GROUP BY p.status")
List<Object[]> countGroupByStatus();
@Query vẫn nằm trong interface — không cần class nào. Xem thêm tại bài JpaRepository & derived queries.
4. ddl-auto — ai kiểm soát schema
spring.jpa.hibernate.ddl-auto quyết định Hibernate được làm gì với database schema lúc startup:
| Giá trị | Hành vi | Dùng khi |
|---|---|---|
none | Không action, không kiểm tra | Migration tool quản lý toàn bộ |
validate | Kiểm tra entity match schema, fail nếu mismatch | Production |
update | Tự thêm column/table còn thiếu, không xóa | Dev local (không bao giờ production) |
create | Drop tất cả + tạo lại | Test (ephemeral) |
create-drop | Giống create, xóa thêm lúc shutdown | Test (ephemeral) |
4.1 Vì sao không dùng ddl-auto=update ở production
update trông tiện nhưng nguy hiểm trên production vì ba lý do:
Race condition multi-pod: khi deploy rolling update (3 pod cùng restart), mỗi pod chạy ddl-auto=update độc lập. Hai pod cùng ALTER TABLE ADD COLUMN priority → lỗi "column already exists" hoặc deadlock lock table.
Không reversible: Hibernate update chỉ biết thêm, không biết xóa hay đổi tên. Đổi tên field trong entity → Hibernate thêm column mới, column cũ vẫn còn, data cũ mất mapping.
Không auditable: schema thay đổi không có record trong git, không review được, không rollback có kiểm soát.
Pattern chuẩn cho production:
# application.yml (production)
spring:
jpa:
hibernate:
ddl-auto: validate # chi xac nhan schema khop entity, khong sua gi
# application-dev.yml (dev local)
spring:
jpa:
hibernate:
ddl-auto: update # tien loi khi prototype, doi schema nhanh
# application-test.yml (integration test)
spring:
jpa:
hibernate:
ddl-auto: create-drop # schema sach sau moi test run
validate giúp phát hiện lỗi ngay khi app khởi động: Flyway migration chạy trước, sau đó Hibernate kiểm tra entity với schema. Nếu một field trong entity không có column tương ứng → app từ chối start thay vì chạy và gây data corruption.
Schema thay đổi đúng quy trình production: viết Flyway migration script (V2__add_project_priority.sql) → commit cùng entity change → CI/CD apply migration → app start với validate pass. Bài 06 đào sâu Flyway setup.
5. Pitfall
Modify entity sau khi transaction kết thúc:
// SAI — service method KHONG @Transactional
public void updateName(Long id, String name) {
Project p = repo.findById(id).orElseThrow();
// tx cua findById da ket thuc, p la Detached
p.setName(name); // KHONG bao gio sync DB
}
// DUNG
@Transactional
public void updateName(Long id, String name) {
Project p = repo.findById(id).orElseThrow();
p.setName(name); // Managed, dirty check active → auto UPDATE khi commit
}
Gọi em.createQuery(...) khi Spring Data đủ dùng:
// SAI — verbose, khong can thiet
@PersistenceContext EntityManager em;
public List<Project> findActive() {
return em.createQuery("SELECT p FROM Project p WHERE p.status = 'ACTIVE'", Project.class)
.getResultList();
}
// DUNG — Spring Data derived query
List<Project> findByStatus(ProjectStatus status);
Chỉ drop xuống EntityManager trực tiếp khi @Query JPQL không đủ biểu đạt (bulk DML, native query phức tạp, dynamic criteria).
ddl-auto=update trên staging "giống production":
Staging environment nên dùng validate + Flyway giống hệt production. Dùng update trên staging sẽ che giấu bug migration, để rồi bùng phát lúc deploy production thật.
Liên hệ các bài khác
- ORM & impedance mismatch: tại sao JPA và EntityManager tồn tại — 5 khác biệt giữa Java object model và SQL relational model mà ORM giải quyết. Đọc trước để hiểu motivation.
- @Entity & @Id strategies: entity class phải map vào table như thế nào —
@Entity,@Id,@GeneratedValue, naming strategy. EntityManager chỉ hoạt động đúng khi entity mapping đúng. - JpaRepository & derived queries: mở rộng cơ chế method name parsing bài này đã giới thiệu — toàn bộ keyword grammar,
@QueryJPQL/native,Pageable,Sort, và khi nào cần xuốngEntityManager.
Tóm tắt
EntityManagerlà core API JPA:persist(INSERT),find(SELECT by PK),merge(UPDATE),remove(DELETE). SQL flush khi commit.- Persistence context = first-level cache trong một transaction: identity guarantee (cùng PK trả cùng instance) + dirty checking (change tự sync khi commit).
- Bốn trạng thái entity: Transient → Managed → Detached → Removed. Sửa entity Detached không sync DB.
- JPQL query entity class/field, không query table/column. Hibernate compile JPQL → SQL theo dialect. Portable qua DB.
- Spring Data sinh repository implementation tại runtime: interface + method name →
PartTreeJpaQueryparse → JPQL template → proxy bean. Không cần viếtEntityManager.createQuerytay. - Derived query fail-fast tại startup nếu property không tồn tại trong entity.
- ddl-auto:
validatecho production (Hibernate chỉ kiểm tra),updatechỉ dev (tự sửa schema, race condition multi-pod),create-dropcho test.
Tự kiểm tra
Q1Gọi em.persist(project) có chạy SQL INSERT ngay không? Giải thích cơ chế deferred write và persistence context flush.▸
em.persist(project) có chạy SQL INSERT ngay không? Giải thích cơ chế deferred write và persistence context flush.Không. em.persist(project) chuyển entity từ trạng thái Transient sang Managed — đưa entity vào persistence context để Hibernate theo dõi — nhưng chưa sinh SQL nào.
SQL INSERT chỉ được ghi xuống DB khi persistence context flush. Điều này xảy ra tự động tại hai thời điểm: (1) transaction commit, (2) trước khi Hibernate thực thi một query để đảm bảo data nhất quán.
Cơ chế deferred write cho phép Hibernate gom nhiều thay đổi (nhiều INSERT, nhiều UPDATE) thành một lần flush, giảm số round-trip đến DB. Đây là lý do dirty checking — Hibernate theo dõi mọi thay đổi trong persistence context và chỉ sinh SQL khi thực sự cần.
Hệ quả thực tế: nếu persist() rồi rollback transaction, không có INSERT nào xảy ra. Nếu muốn flush sớm (ví dụ cần ID sau persist), gọi em.flush() thủ công.
Q2Persistence context đảm bảo gì về identity? Tại sao điều đó quan trọng?▸
Persistence context đảm bảo identity guarantee: trong cùng một transaction, mọi find/findById với cùng PK trả về cùng một Java instance (tham chiếu == bằng nhau).
Hibernate duy trì một Map từ (entity class, PK) sang entity instance. Lần gọi thứ hai tìm thấy entry trong map nên trả ngay, không sinh SQL thêm.
Quan trọng vì: (1) tránh N query không cần thiết, (2) đảm bảo code thao tác trên "cùng một object" thay vì hai bản sao có thể drift khác nhau, (3) dirty checking hoạt động đúng — Hibernate chỉ cần theo dõi một instance per PK.
Đây là L1 cache, khác L2 cache (cross-transaction, phải cấu hình thêm, không bật mặc định). L1 luôn hoạt động, không opt-out được, và bị xóa khi transaction kết thúc.
Q3JPQL khác SQL ở điểm cốt lõi nào? Vì sao SELECT p FROM Project p WHERE p.status = :s không phải SQL?▸
SELECT p FROM Project p WHERE p.status = :s không phải SQL?Điểm khác cốt lõi: JPQL query entity class và Java field, không query table và column SQL.
Trong câu trên: Project là tên Java class (có annotation @Entity), p.status là tên field Java trong class đó — không phải tên table projects hay tên column status.
Hibernate nhận JPQL, tra mapping (@Table, @Column, naming strategy), dịch sang SQL cụ thể cho dialect đang cấu hình: PostgreSQL, MySQL, Oracle đều nhận SQL syntax riêng, nhưng JPQL không đổi.
Lợi ích: portable qua DB (đổi DB chỉ đổi config, không sửa query), object navigation tự nhiên (p.customer.region thay vì viết JOIN tay), fail tại startup khi field không tồn tại. Hạn chế: không dùng được tính năng DB-specific — lúc đó cần nativeQuery = true.
Q4Spring Data JPA sinh implementation cho findByStatusAndCreatedAtAfter bằng cách nào? Lỗi gì xảy ra nếu viết findByStatuz?▸
findByStatusAndCreatedAtAfter bằng cách nào? Lỗi gì xảy ra nếu viết findByStatuz?Tại startup, PartTreeJpaQuery parse tên method theo grammar: [verb]By[property][keyword][And/Or][property][keyword].
Với findByStatusAndCreatedAtAfter: verb = find, criteria = status (Equals ẩn) + createdAt After. Kết quả JPQL: SELECT p FROM Project p WHERE p.status = ?1 AND p.createdAt > ?2. Template này được cache lại — mỗi lần gọi method chỉ bind parameter và execute, không parse lại.
Proxy được đăng ký như Spring bean. Khi service inject ProjectRepository, nó nhận proxy này. Mọi lời gọi method đi qua proxy → PartTreeJpaQuery → EntityManager → SQL.
Nếu viết findByStatuz (typo): Spring Data không tìm thấy property statuz trong entity Project nên ném PropertyReferenceException: No property 'statuz' found for type 'Project' tại startup, không phải lúc runtime. App từ chối khởi động — fail-fast có chủ đích.
Q5ddl-auto=update trông tiện lợi — tự thêm column khi entity thay đổi. Tại sao không dùng ở production?▸
ddl-auto=update trông tiện lợi — tự thêm column khi entity thay đổi. Tại sao không dùng ở production?Ba lý do cụ thể:
Race condition multi-pod: rolling deploy với 3 pod cùng restart, mỗi pod chạy ddl-auto=update độc lập, đều cố ALTER TABLE ADD COLUMN priority. Hai pod chạy đồng thời → lỗi "column already exists" hoặc deadlock lock table, khiến pod fail và deploy hỏng.
Không reversible / không audit: Hibernate update chỉ biết thêm, không biết xóa hay rename. Đổi tên field trong entity → Hibernate thêm column mới, column cũ còn nguyên với data cũ không map. Schema diverge khỏi entity mà không ai biết. Không có record trong git, không review được, không rollback có kiểm soát.
Che giấu bug migration: nếu dùng update trên staging, Flyway migration script có thể sai mà staging vẫn "pass" vì Hibernate tự sửa. Lỗi bùng phát đúng lúc deploy production nơi schema không được tự sửa.
Production đúng: ddl-auto=validate + Flyway. Flyway apply migration trước (versioned, reviewable, rollback-able), Hibernate sau đó validate entity khớp schema. Lỗi lộ ngay startup, không lúc user dùng.
Bài tiếp theo: @Entity & @Id strategies
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên