SPRING SERIES - LỖI IM LẶNG

Read-Only: Tắt Người Chép, Không Tự Dựng Rào

PROXY - XANH = TẦNG PHÓNG TO Đọc từ trái sang phải. Màu đậm dần nghĩa là gần cơ chế hơn. Dấu * đá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.

Cùng một gốcMột transaction read-only mới đổi hành vi Hibernate và gửi hint tới JDBC. Annotation tự nó không phải quyền từ chối DML.

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.

Mức 1 - around advice đồng bộ

TransactionInterceptor.invoke()

Đường proxy chuẩn tới transaction manager

  1. invokeWithinTransaction()Đọc TransactionAttribute, trong đó có cờ readOnly.
  2. createTransactionIfNecessary()Chuẩn bị TransactionInfo cho lời gọi hiện tại.
  3. tm.getTransaction(txAttr)Propagation quyết định mở mới hay tham gia transaction đang có.
  4. JpaTransactionManager.doBegin()Chỉ khi mở transaction mới, manager mới áp read-only xuống resource.
  5. invocation.proceedWithInvocation()Target method đọc entity hoặc phát DML tường minh.
  6. Commit, rollback và cleanupKết thúc transaction rồi khôi phục trạng thái resource đã đổi.
Mức 2 - zoom lúc mở JPA transaction

JpaTransactionManager.doBegin()

Definition đi vào dialect, EntityManager và JDBC cùng transaction

  1. createEntityManagerForTransaction()Mở transaction-local EntityManager nếu chưa có holder phù hợp.
  2. getJpaDialect().beginTransaction()Chuyển JpaTransactionDefinition cho HibernateJpaDialect.
  3. txObject.setReadOnly(definition.isReadOnly())Ghi trạng thái để cleanup đúng sau khi hoàn tất.
  4. getJdbcConnection(em, readOnly)Lấy handle tới Connection của Hibernate khi có DataSource.
  5. bindResource(dataSource, conHolder)Cho JdbcTemplate dùng chung JDBC transaction với JPA.
  6. bindResource(entityManagerFactory, emHolder)Gắn EntityManager mới vào thread cho phần business.
Mức 3 - zoom hai nơi nhận hint

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 sang MANUAL, 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 khi prepareConnection=true và 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: readOnlyMode quyết địnhtransaction gửi BEGIN READ ONLY; ignore là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=true cho 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ờ REQUIRED sử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 UPDATE nghĩ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-writeREQUIRED mặ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, DataSourceTransactionManager có tùy chọn enforceReadOnly=true.