Spring Data JPA & Hibernate

Cheat sheet tổng ôn tập: mapping, quan hệ, vòng đời entity, repository, truy vấn, N+1, transaction, lock, cache và những bẫy hay gặp. Viết cho Spring Boot 3 / Hibernate 6.

  1. Spring Data JPAJpaRepository, @Query, Specification, Pageable
  2. JPA (Jakarta Persistence)EntityManager, @Entity, JPQL, vòng đời entity
  3. HibernateSession, dirty checking, sinh SQL, cache, lazy proxy
  4. JDBC + HikariCPConnection pool, PreparedStatement
  5. DatabaseBảng, index, khóa, isolation
Bộ cheatsheet: 🏠 Trang chủ ☕ Java Core 🌱 Spring Core 🟢 Spring Data JPA & Hibernate

Tổng quan

Ba thứ hay bị nhầm thành một. Nắm rõ ranh giới này là câu hỏi mở đầu của hầu hết buổi phỏng vấn.

JPA, Hibernate và Spring Data JPA khác nhau thế nào

Thành phầnLà gìAPI tiêu biểu
JPAĐặc tả chuẩn (interface và annotation), tự nó không chạy được@Entity EntityManager JPQL
HibernateImplementation phổ biến nhất của JPA, có thêm tính năng riêngSession @BatchSize @NaturalId HQL
Spring Data JPALớp trừu tượng của Spring, tự sinh implementation cho repository dựa trên JPAJpaRepository @Query Specification

Luồng gọi: Service → Repository (proxy, thực chất là SimpleJpaRepository) → EntityManager → Hibernate Session → JDBC → DB.

Spring Boot 3 / Hibernate 6: điểm khác

  • Package là jakarta.persistence.*, không còn javax.persistence.*
  • Cần Java 17 trở lên
  • Hibernate tự nhận dialect, thường không cần khai báo
  • getReferenceById() thay cho getOne()
  • ListCrudRepository trả List thay vì Iterable
  • PagingAndSortingRepository không còn extends CrudRepository
  • @Where được thay bằng @SQLRestriction (Hibernate 6.3+)

Khi nào dùng cái gì

  • CRUD đơn giản, truy vấn theo tên field: derived query
  • Truy vấn có join hoặc điều kiện phức tạp: @Query JPQL
  • Filter động nhiều điều kiện tùy chọn: Specification
  • Báo cáo, hàm đặc thù DB: native query hoặc projection
  • Cần kiểm soát sâu session, cache, statistics: unwrap sang Session

Cấu hình

Setup tối thiểu và những property đáng nhớ.

Dependency

application.yml

ddl-auto

Giá trịHành vi
noneKhông làm gì
validateKiểm tra schema khớp entity, lệch thì báo lỗi lúc khởi động
updateThêm bảng/cột còn thiếu, không bao giờ xóa. Chỉ dùng khi dev
createDrop rồi tạo lại mỗi lần chạy
create-dropNhư create, và drop khi tắt app. Hợp với test

Production: dùng validate hoặc none kết hợp Flyway / Liquibase để quản lý migration.

Bật statistics để đếm query

Cách nhanh nhất để phát hiện N+1: đếm số câu SQL cho một request.

Mapping entity

Annotation cơ bản, chiến lược sinh khóa, kế thừa và các kiểu nhúng.

Entity mẫu

Yêu cầu với entity: có @Id, có constructor không tham số (public hoặc protected), class và field không final (để tạo proxy lazy), không dùng record làm entity.

Annotation hay dùng

AnnotationÝ nghĩa
@EntityĐánh dấu class là entity
@TableTên bảng, constraint, index
@Columnname, nullable, length, unique, updatable, insertable
@EnumeratedSTRING hoặc ORDINAL
@TemporalChỉ cho java.util.Date. Kiểu java.time không cần
@TransientBỏ qua field khi persist
@LobCLOB / BLOB
@ConvertDùng AttributeConverter để đổi kiểu
@VersionCột version cho optimistic lock
@MappedSuperclassClass cha chia sẻ field, bản thân không phải entity

Chiến lược sinh khóa

StrategyĐặc điểm
IDENTITYAuto-increment của DB. Phải INSERT ngay để lấy id nên tắt JDBC batch insert
SEQUENCEDùng sequence, allocationSize mặc định 50, hỗ trợ batch. Lựa chọn tốt cho PostgreSQL, Oracle
UUIDSinh UUID phía app (JPA 3.1). Index kém hơn số tự tăng
TABLEGiả lập sequence bằng bảng. Chậm, nên tránh
AUTOProvider tự chọn theo DB, dễ gây bất ngờ

Kế thừa

KiểuĐặc điểm
SINGLE_TABLEMặc định. Một bảng + cột DTYPE. Nhanh, nhưng cột của lớp con phải nullable
JOINEDMỗi lớp một bảng, join khi đọc. Chuẩn hóa tốt, chậm hơn
TABLE_PER_CLASSMỗi lớp cụ thể một bảng, truy vấn đa hình dùng UNION. Ít dùng
@MappedSuperclassKhông đa hình. Chỉ để dùng lại field

Embedded và khóa phức hợp

AttributeConverter

Quan hệ giữa các entity

Phần dễ sai nhất. Nhớ ba khái niệm: bên sở hữu (owning side), cascade và fetch.

Mặc định fetch

Quan hệMặc định
@ManyToOneEAGER
@OneToOneEAGER
@OneToManyLAZY
@ManyToManyLAZY

Khuyến nghị: đặt fetch = LAZY cho tất cả, rồi chủ động fetch bằng join fetch hoặc EntityGraph khi cần.

Owning side và mappedBy

  • Bên sở hữu là bên giữ khóa ngoại, thường là phía @ManyToOne
  • Bên inverse khai báo mappedBy = "tênFieldBênSởHữu"
  • Chỉ thay đổi ở bên sở hữu mới được ghi xuống DB
  • Quan hệ hai chiều phải đồng bộ cả hai phía bằng helper method

Một-nhiều hai chiều (mẫu chuẩn)

Nhiều-nhiều

Dùng Set thay vì List để tránh Hibernate xóa hết rồi chèn lại bảng nối. Khi bảng nối cần thêm cột, tách thành entity trung gian với hai @ManyToOne.

Một-một dùng chung khóa chính

@OneToOne phía inverse (có mappedBy) khó lazy thật sự vì Hibernate phải truy vấn để biết là null hay không. Dùng @MapsId là cách gọn nhất.

CascadeType

LoạiLan truyền thao tác
PERSISTpersist cha thì persist con
MERGEmerge cha thì merge con
REMOVExóa cha thì xóa con
REFRESH / DETACHrefresh / detach lan sang con
ALLTất cả các loại trên

orphanRemoval = true: gỡ con khỏi collection của cha thì con bị xóa khỏi DB.

Không đặt CascadeType.REMOVE hoặc ALL trên @ManyToMany: xóa một phía sẽ kéo theo xóa entity ở phía kia.

Bẫy hay gặp ở quan hệ

  • @OneToMany một chiều không có @JoinColumn sinh thêm bảng nối và câu UPDATE thừa
  • toString(), hashCode(), JSON serialize đi vòng giữa hai phía gây StackOverflowError
  • Quên gọi helper nên chỉ set một phía, DB không thấy thay đổi
  • Trả entity thẳng ra API làm lộ lazy proxy. Dùng DTO

Vòng đời entity và Persistence Context

Persistence context là “vùng nhớ làm việc” của EntityManager: nó theo dõi các entity managed và là cache cấp 1.

Bốn trạng thái

Transient / New chưa có trong context Managed dirty checking tự động Detached có id, hết theo dõi Removed DELETE khi flush Database persist() detach() / clear() / close() merge() remove() find() / query flush / commit

Một entity chỉ được theo dõi khi đang Managed. merge() trả về một bản Managed mới, còn object bạn truyền vào vẫn là Detached.

Thao tác của EntityManager

Phương thứcTác dụng
persist(e)New → Managed. Chính instance đó trở thành managed
find(C, id)Tìm trong context trước, không có mới query. Trả null nếu không thấy
getReference(C, id)Trả proxy, chưa query. Lỗi EntityNotFoundException khi truy cập mà không tồn tại
merge(e)Copy state của detached vào managed instance, trả về bản managed
remove(e)Managed → Removed
refresh(e)Nạp lại state từ DB, ghi đè thay đổi chưa flush
flush()Đẩy SQL xuống DB, chưa commit
detach / clearBỏ một / toàn bộ entity khỏi context

Dirty checking

Entity managed bị sửa thì Hibernate tự UPDATE khi flush. Không cần gọi save().

Flush xảy ra: khi commit, trước khi chạy query có thể bị ảnh hưởng (FlushMode AUTO), hoặc khi gọi flush() thủ công. Transaction readOnly = true đặt flush mode MANUAL nên bỏ qua dirty checking.

Cache cấp 1 trong thực tế

Phạm vi của cache cấp 1 là một EntityManager (thường là một transaction).

Lifecycle callback

@PrePersist @PostPersist @PreUpdate @PostUpdate @PreRemove @PostRemove @PostLoad

Dùng merge đúng cách

Spring Data JPA Repository

Chỉ cần khai báo interface, Spring tự tạo implementation lúc khởi động.

Hệ phân cấp interface

InterfaceBổ sung
Repository<T,ID>Marker, không có method
CrudRepositorysave, findById, existsById, count, delete…
ListCrudRepositoryNhư trên nhưng trả List
PagingAndSortingRepositoryfindAll(Sort), findAll(Pageable)
JpaRepositoryflush, saveAndFlush, deleteAllInBatch, getReferenceById

Method CRUD có sẵn

MethodGhi chú
save / saveAllpersist nếu mới, merge nếu đã có id. Luôn dùng giá trị trả về
findByIdTrả Optional
getReferenceByIdProxy lazy, tiện để gán FK
findAll / findAllByIdCẩn thận với bảng lớn
deleteById / deleteLoad entity rồi xóa
deleteAllInBatchMột câu DELETE duy nhất, không load
deleteAllLoad rồi xóa từng entity, chậm
saveAndFlush / flushÉp đẩy SQL xuống DB ngay

save() quyết định mới hay cũ thế nào

  • Mới (isNew): id null, hoặc cột @Version null → persist
  • Còn lại → merge (có thể sinh thêm SELECT)
  • Id tự gán tay (không dùng @GeneratedValue) làm mọi lần save đều thành merge, thừa một SELECT. Cách xử lý: implement Persistable<ID> hoặc thêm @Version

Từ khóa derived query

Từ khóaVí dụ
And / OrfindByNameAndAge
BetweenfindByAgeBetween(a, b)
LessThan / GreaterThan (Equal)findByAgeGreaterThanEqual
After / BeforefindByCreatedAtAfter
IsNull / IsNotNullfindByDeletedAtIsNull
LikeTự truyền ký tự %
StartingWith / EndingWith / ContainingTự thêm %
In / NotInfindByRoleIn(Collection)
True / FalsefindByActiveTrue
IgnoreCasefindByEmailIgnoreCase
OrderBy…Asc/DescfindByRoleOrderByNameDesc
First / Top N, DistinctfindTop5By…
countBy / existsBy / deleteByexistsByEmail

Ví dụ derived query

@Query, tham số và update hàng loạt

@Modifying chạy thẳng xuống DB, bỏ qua persistence context nên entity đang giữ trong bộ nhớ có thể cũ. Dùng clearAutomatically = true để xóa context sau khi chạy.

Phân trang và sắp xếp

Kiểu trả vềChi phí
PageThêm một câu COUNT để biết tổng
SliceLấy size+1 dòng, không COUNT. Hợp với “tải thêm”
List + PageableChỉ lấy dữ liệu, không metadata

Chỉ số trang bắt đầu từ 0.

Projection: chỉ lấy cột cần

Open projection (@Value("#{target.a + target.b}")) phải load cả entity nên mất lợi thế.

Specification (filter động)

Trả null nghĩa là “không thêm điều kiện”. Ngoài ra có Query by Example (repo.findAll(Example.of(probe))) cho trường hợp lọc đơn giản theo ví dụ.

@EntityGraph

Cách khai báo fetch ngay trên method mà không cần viết JPQL.

Custom repository

Tên class implementation phải kết thúc bằng Impl.

JPQL nhanh

Viết trên entity và field, không phải bảng và cột.

Các mẫu hay dùng

Hàm có sẵn: lower upper concat length coalesce current_date và các hàm gom nhóm count sum avg min max.

Join fetch một collection kèm phân trang: Hibernate cảnh báo (HHH90003004) và phân trang trong bộ nhớ, rất tốn RAM. Cách xử lý: query 1 lấy id theo trang, query 2 fetch theo danh sách id; hoặc dùng @BatchSize.

Fetch & vấn đề N+1

Nguồn gốc của phần lớn vấn đề hiệu năng khi dùng JPA.

N+1 là gì

Phát hiện: bật org.hibernate.SQL hoặc statistics và đếm số câu SQL cho một request.

Cách khắc phục

CáchKhi dùng
JOIN FETCHBiết trước cần quan hệ nào, viết JPQL
@EntityGraphNhư trên nhưng khai báo bằng annotation
@BatchSize / default_batch_fetch_sizeGom N câu thành N/size câu dạng IN (...)
Projection / DTOChỉ cần vài cột, không cần entity
@Fetch(SUBSELECT)Nạp collection của tất cả entity bằng một subquery

Ví dụ sửa

MultipleBagFetchException

Xảy ra khi join fetch hai List (bag) trong cùng một query. Cách xử lý:

  • Đổi một trong hai sang Set
  • Hoặc fetch từng collection bằng query riêng trong cùng transaction
  • Hoặc dùng @BatchSize thay cho join fetch

Fetch nhiều collection cùng lúc còn gây tích Descartes, số dòng nhân lên.

LazyInitializationException

Truy cập quan hệ lazy khi session đã đóng (thường ngoài transaction, hoặc ở controller).

  • Fetch đủ dữ liệu trong service bằng join fetch / EntityGraph
  • Map sang DTO trước khi ra khỏi tầng service
  • Không “chữa” bằng cách chuyển sang EAGER hay bật open-in-view

spring.jpa.open-in-view mặc định là true: giữ EntityManager mở suốt request, che lỗi lazy nhưng giữ connection lâu và dễ sinh query ngầm ở tầng view. Nên đặt false.

getReferenceById để gán FK

Không cần load nguyên entity chỉ để gán khóa ngoại.

Transaction

@Transactional hoạt động qua proxy AOP. Hiểu cơ chế proxy là hiểu phần lớn bẫy.

Thuộc tính chính

Thuộc tínhÝ nghĩa
propagationCách tham gia transaction hiện có. Mặc định REQUIRED
isolationMức cô lập. Mặc định là của DB
readOnlyGợi ý chỉ đọc, bỏ dirty checking, tối ưu
timeoutGiây tối đa
rollbackFor / noRollbackForTùy biến exception nào gây rollback

Mặc định chỉ rollback với RuntimeException và Error. Checked exception không rollback, trừ khi khai báo rollbackFor = Exception.class.

Propagation

LoạiHành vi
REQUIREDCó thì tham gia, chưa có thì tạo mới
REQUIRES_NEWLuôn tạo transaction mới, treo cái cũ
SUPPORTSCó thì tham gia, không thì chạy không transaction
NOT_SUPPORTEDTạm ngưng transaction hiện có
MANDATORYBắt buộc đã có, không thì lỗi
NEVERCó transaction thì lỗi
NESTEDTransaction lồng bằng savepoint

Isolation và hiện tượng đọc

MứcDirtyNon-repeatablePhantom
READ_UNCOMMITTEDCóCóCó
READ_COMMITTEDKhôngCóCó
REPEATABLE_READKhôngKhôngCó*
SERIALIZABLEKhôngKhôngKhông

* Theo chuẩn SQL. Một số DB như MySQL InnoDB chặn phần lớn phantom ở mức này.

Khi @Transactional không chạy

  • Self-invocation: gọi this.method() trong cùng class không đi qua proxy
  • Method private, final hoặc static
  • Bean không do Spring quản lý (tự new)
  • Checked exception mà không có rollbackFor
  • catch exception rồi nuốt mất, transaction thấy như thành công
  • Chạy sang thread khác (@Async, parallel stream)

Đặt ở đâu

Đặt ở tầng service, không đặt ở controller hay repository. SimpleJpaRepository đã có sẵn @Transactional(readOnly = true) ở cấp class và @Transactional ở method ghi.

Lock và đồng thời

Hai cách chống lost update: lạc quan (optimistic) và bi quan (pessimistic).

Optimistic lock

Hibernate sinh UPDATE ... WHERE id = ? AND version = ?. Nếu không có dòng nào bị ảnh hưởng, ném OptimisticLockException (Spring dịch thành ObjectOptimisticLockingFailureException).

  • Không giữ khóa DB, hợp khi xung đột hiếm
  • Xử lý bằng cách retry hoặc báo người dùng tải lại

Pessimistic lock

Sinh SELECT ... FOR UPDATE. Phải chạy trong transaction. Hợp khi xung đột thường xuyên, nhưng dễ deadlock và giảm throughput.

LockModeType

LoạiÝ nghĩa
OPTIMISTICKiểm tra version lúc commit (đọc)
OPTIMISTIC_FORCE_INCREMENTTăng version dù entity không đổi
PESSIMISTIC_READKhóa chia sẻ, người khác đọc được nhưng không ghi
PESSIMISTIC_WRITEKhóa độc quyền (FOR UPDATE)
PESSIMISTIC_FORCE_INCREMENTKhóa độc quyền và tăng version

Cache

Cache cấp 1 và cấp 2

Cấp 1Cấp 2
Phạm viMột EntityManager / SessionToàn SessionFactory, dùng chung nhiều session
Mặc địnhLuôn bậtTắt, cần provider
LưuChính entityDạng dữ liệu rời (dehydrated)

Bật cache cấp 2 và query cache

Chỉ nên cache dữ liệu đọc nhiều, ít đổi (danh mục, cấu hình). Đừng nhầm với @Cacheable của Spring Cache, đó là cơ chế khác, cache ở tầng method.

Tính năng riêng của Hibernate

Không nằm trong chuẩn JPA, dùng được vì Hibernate là implementation phía dưới.

Annotation thường gặp

AnnotationTác dụng
@DynamicUpdateUPDATE chỉ các cột thực sự đổi
@NaturalIdKhóa tự nhiên (email, mã…), tối ưu lookup có cache
@FormulaField tính từ biểu thức SQL, chỉ đọc
@BatchSizeGom lazy-load thành lô
@FetchSELECT, JOIN hoặc SUBSELECT
@ImmutableEntity không bao giờ UPDATE
@CreationTimestamp / @UpdateTimestampTự gán thời gian
@UuidGeneratorTùy biến cách sinh UUID
@FilterBộ lọc bật tắt theo session

Soft delete

Gọi delete() sẽ thành UPDATE, mọi query tự thêm điều kiện deleted = false.

Truy cập Session

Thuật ngữ tương ứng: EntityManager ↔ Session, EntityManagerFactory ↔ SessionFactory, JPQL ↔ HQL.

Batch insert / update

  • Đặt hibernate.jdbc.batch_size (ví dụ 50)
  • Bật order_inserts và order_updates
  • Dùng SEQUENCE, không dùng IDENTITY
  • Với lượng rất lớn: flush() rồi clear() theo từng lô để context không phình ra

Auditing

Tự ghi thời gian và người tạo/sửa

Nên và tránh

Danh sách rà soát nhanh trước khi review code hoặc đi phỏng vấn.

Nên

Đặt mọi quan hệ ở LAZY, fetch theo nhu cầu.
Viết helper add/remove cho quan hệ hai chiều.
Dùng @Enumerated(EnumType.STRING).
Dùng DTO / projection ở biên API.
Dùng Long, Integer cho id (cho phép null).
Đặt @Transactional(readOnly = true) cho luồng đọc.
Dùng SEQUENCE khi cần batch insert.
Tắt open-in-view.
Quản lý schema bằng Flyway / Liquibase.
Phân trang mọi danh sách có thể lớn.

Tránh

Trả entity trực tiếp ra REST.
findAll() rồi lọc bằng Java stream.
Lombok @Data, @EqualsAndHashCode, @ToString trên entity có quan hệ.
EAGER để “cho khỏi lỗi lazy”.
@Enumerated(ORDINAL).
ddl-auto: update trên production.
deleteAll() cho bảng lớn (dùng deleteAllInBatch hoặc query xóa).
@ManyToMany với CascadeType.REMOVE.
Bắt exception trong method @Transactional rồi bỏ qua.

equals và hashCode cho entity

Hoặc dùng khóa nghiệp vụ (@NaturalId) nếu có. Tránh so sánh trên mọi field vì kéo theo lazy collection và id thay đổi sau khi persist làm hỏng HashSet.

Hỏi nhanh đáp gọn

Bấm vào từng câu để xem đáp án. Thử tự trả lời trước khi mở.

persist() khác merge() thế nào?

persist nhận entity mới (transient), đưa chính instance đó vào managed; truyền entity detached sẽ lỗi. merge copy state vào một instance managed (có thể SELECT trước) và trả về bản managed, còn object truyền vào vẫn detached.

save() của Spring Data làm gì?

Entity mới (id null hoặc version null) thì gọi persist, ngược lại gọi merge. Luôn dùng giá trị trả về. save không flush ngay (trừ IDENTITY phải INSERT lấy id); saveAndFlush thì flush ngay.

LAZY khác EAGER, mặc định ra sao?

LAZY nạp khi truy cập lần đầu (qua proxy hoặc collection wrapper), EAGER nạp cùng entity cha. Mặc định: @ManyToOne, @OneToOne là EAGER; @OneToMany, @ManyToMany là LAZY. Nên đặt LAZY hết và fetch chủ động.

N+1 là gì, sửa thế nào?

1 query lấy danh sách rồi N query nạp quan hệ lazy cho từng phần tử. Sửa bằng JOIN FETCH, @EntityGraph, @BatchSize / default_batch_fetch_size hoặc dùng DTO projection.

@Transactional không có tác dụng khi nào?

Self-invocation (gọi nội bộ qua this), method private/final/static, bean không do Spring quản lý, checked exception không có rollbackFor, exception bị catch nuốt, hoặc chạy ở thread khác.

mappedBy dùng để làm gì, owning side là gì?

Owning side là phía giữ khóa ngoại và là phía duy nhất quyết định dữ liệu ghi xuống DB. Phía còn lại (inverse) dùng mappedBy trỏ về field của owning side để không tạo thêm cột hay bảng nối.

orphanRemoval khác CascadeType.REMOVE?

REMOVE xóa con khi xóa cha. orphanRemoval = true xóa con khi con bị gỡ khỏi collection của cha (và cả khi cha bị xóa).

Optimistic hay pessimistic lock?

Optimistic dùng @Version, không khóa DB, phát hiện xung đột lúc ghi rồi retry; hợp khi xung đột ít. Pessimistic dùng SELECT ... FOR UPDATE, chặn người khác; hợp khi xung đột nhiều nhưng dễ deadlock và giảm thông lượng.

Page, Slice và List khác nhau thế nào?

Page chạy thêm COUNT để có tổng số phần tử và số trang. Slice chỉ biết còn trang sau hay không (lấy size+1), không COUNT. List chỉ trả dữ liệu.

getReferenceById khác findById?

findById query ngay và trả Optional. getReferenceById trả proxy chưa query, tiện để gán FK; nếu bản ghi không tồn tại thì lỗi chỉ xuất hiện khi truy cập dữ liệu.

JPQL, native query và Specification chọn cái nào?

JPQL cho truy vấn tĩnh viết trên entity, độc lập DB. Native khi cần hàm hoặc cú pháp riêng của DB. Specification (Criteria API) khi điều kiện lọc ghép động từ nhiều tùy chọn.

Cache cấp 1 khác cấp 2?

Cấp 1 là persistence context, luôn bật, phạm vi một session/transaction. Cấp 2 dùng chung giữa các session, mặc định tắt, cần provider và cấu hình từng entity.

Vì sao entity cần constructor không tham số?

Hibernate tạo instance bằng reflection khi nạp dữ liệu, và sinh proxy lazy bằng cách tạo lớp con. Vì vậy class không được final và constructor nên là public hoặc protected.

Vì sao IDENTITY làm mất batch insert?

Hibernate cần id ngay sau khi persist, mà với IDENTITY id chỉ có sau khi INSERT chạy, nên buộc phải thực thi từng câu. SEQUENCE lấy id trước nên gom được lô.

Vì sao nên tắt open-in-view?

Nó giữ EntityManager (và thường là connection) suốt vòng đời request, che lỗi LazyInitializationException và cho phép query ngầm ở tầng view/serialize. Tắt đi buộc bạn fetch rõ ràng trong service, tránh cạn connection pool.

Dirty checking hoạt động ra sao?

Khi nạp entity, Hibernate lưu một bản snapshot. Lúc flush nó so sánh trạng thái hiện tại với snapshot; có khác biệt thì sinh UPDATE. Vì vậy sửa entity managed trong transaction là đủ, không cần gọi save.