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
| 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.

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?

Quá 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. - Q2Persistence context đảm bảo gì về identity? Tại sao điều đó quan trọng?
- Q3JPQL khác SQL ở điểm cốt lõi nào? Vì sao
SELECT p FROM Project p WHERE p.status = :skhông phải SQL? - Q4Spring Data JPA sinh implementation cho
findByStatusAndCreatedAtAfterbằng cách nào? Lỗi gì xảy ra nếu viếtfindByStatuz? - Q5
ddl-auto=updatetrông tiện lợi — tự thêm column khi entity thay đổi. Tại sao không dùng ở production?
Bài tiếp theo: @Entity & @Id strategies
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
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