SPRING SERIES - LỖI IM LẶNG
Read-Only: Tắt Người Chép, Không Tự Dựng Rào
* đánh dấu điểm bài viết khai thác.
Ghi tường minh - có thể vẫn qua
JdbcTemplate.update() hoặc native DML vẫn chạy nếu driver và database không cưỡng chế read-only.
Ghi ngầm - có thể bốc hơi
Sửa entity vừa load nhưng không gọi DML tường minh; Hibernate không dirty-check và không tự phát UPDATE.
TransactionInterceptor.invoke()
Đường proxy chuẩn tới transaction manager
invokeWithinTransaction()ĐọcTransactionAttribute, trong đó có cờreadOnly.createTransactionIfNecessary()Chuẩn bịTransactionInfocho lời gọi hiện tại.tm.getTransaction(txAttr)Propagation quyết định mở mới hay tham gia transaction đang có.JpaTransactionManager.doBegin()Chỉ khi mở transaction mới, manager mới áp read-only xuống resource.invocation.proceedWithInvocation()Target method đọc entity hoặc phát DML tường minh.- Commit, rollback và cleanupKết thúc transaction rồi khôi phục trạng thái resource đã đổi.
JpaTransactionManager.doBegin()
Definition đi vào dialect, EntityManager và JDBC cùng transaction
createEntityManagerForTransaction()Mở transaction-localEntityManagernếu chưa có holder phù hợp.getJpaDialect().beginTransaction()ChuyểnJpaTransactionDefinitionchoHibernateJpaDialect.txObject.setReadOnly(definition.isReadOnly())Ghi trạng thái để cleanup đúng sau khi hoàn tất.getJdbcConnection(em, readOnly)Lấy handle tới Connection của Hibernate khi có DataSource.bindResource(dataSource, conHolder)ChoJdbcTemplatedùng chung JDBC transaction với JPA.bindResource(entityManagerFactory, emHolder)Gắn EntityManager mới vào thread cho phần business.
HibernateJpaDialect.beginTransaction()
Một definition, hai cơ chế khác nhau
Nhánh Hibernate - ngừng tự chép
entityManager.getTransaction().begin()Bắt đầu JPA transaction sau khi chuẩn bị Connection nếu cần.prepareFlushMode(session, true)Đổi Hibernate flush mode sangMANUAL, nên commit không tự flush thay đổi.session.setDefaultReadOnly(true)Với transaction-local EntityManager, entity load sau đó không giữ snapshot và không dirty-check.
Nhánh JDBC - gửi hint, không hứa cưỡng chế
DataSourceUtils.prepareConnectionForTransaction()Chạy khiprepareConnection=truevà release mode làON_CLOSE.Connection.setReadOnly(true)Lỗi không hỗ trợ được bỏ qua, trừ lỗi có dạng timeout; driver quyết định hiệu lực.- pgJDBC:
readOnlyModequyết địnhtransactiongửiBEGIN READ ONLY;ignorelàm cờ không có hiệu lực.
Phép kiểm: chạy một transaction mới, breakpoint tại HibernateJpaDialect.beginTransaction(); xem definition.isReadOnly(), flush mode, session.isDefaultReadOnly() và connection.isReadOnly(). Bật SQL, thử riêng một entity mutation không explicit DML và một lệnh INSERT tường minh.
Dùng đúng quyền hạn
- Dùng
readOnly=truecho tối ưu đọcNó giảm công việc flush/dirty-check trên đường Hibernate đã kiểm chứng. - Muốn chặn ghi, dựng rào ở databaseDùng credentials/DataSource chỉ có quyền đọc, hoặc khóa và test cấu hình read-only của đúng driver/database.
- Cần đổi đặc tính transaction lồng nhauĐừng trông chờ
REQUIREDsửa transaction đang có; thiết kế boundary mới khi thật sự cần.
Khi KHÔNG được kết luận
- Không thấy auto
UPDATEnghĩa là DB đã khóa ghiThí nghiệm đó chỉ chứng minh đường dirty-check/flush của provider bị tắt trên stack đã test. - Inner read-only luôn đổi outer read-write
REQUIREDmặc định tham gia transaction hiện có; inner setting không mở lại resource. - Spring không bao giờ gửi SQL read-onlyNgoài đường JPA này,
DataSourceTransactionManagercó tùy chọnenforceReadOnly=true.