使用 Hibernate 實作「軟刪除 + 新增新版本」邏輯:將舊照片記錄 isAvailable 從 'Y' 改為 'N',預期 initTime 不變,但實際執行後舊記錄的 initTime 被更新為比新記錄晚約 20 秒。
核心問題在於 Hibernate Session 的實體狀態管理 與 Spring 事務邊界 的交互:
getImageInfoByUserIdAndType 查詢返回的 TImageInfo 實體處於 Persistent(持久)狀態,受 Session 管理。setIsNew(IS_NEW_VALUE_NO) 並呼叫 saveOrUpdate,Hibernate 會將此實體標記為 dirty。initTime 定義為 @Version、@Generated(INSERT) 或資料庫欄位有 ON UPDATE CURRENT_TIMESTAMP,Hibernate 在 flush 時可能重新產生或資料庫自動更新該欄位。save(newEntity),Session flush 時會一次性執行所有 SQL,順序不保證,導致時間差異。// 檢查註解
@Column(name = "initTime", updatable = false) // 關鍵:禁止 Hibernate 更新
@CreationTimestamp // 僅插入時自動賦值
private Timestamp initTime;
// 或 XML 映射
若欄位定義允許更新(update="true" 或無 updatable=false),Hibernate 會在 dirty check 時包含該欄位。
-- MySQL 範例:檢查欄位定義
SHOW CREATE TABLE t_image_info;
-- 若看到:initTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-- 則資料庫層面會自動更新,必須移除 ON UPDATE 子句
ALTER TABLE t_image_info MODIFY initTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
// 將舊記錄設為不可用,但不觸發完整 dirty check
for (BaseModel item : imagesResult) {
TImageInfo old = (TImageInfo) item;
old.setIsNew(IS_NEW_VALUE_NO);
// 使用 HQL 直接更新單一欄位,避免載入完整實體
getSession().createQuery(
"UPDATE TImageInfo SET isNew = :val WHERE id = :id")
.setParameter("val", IS_NEW_VALUE_NO)
.setParameter("id", old.getId())
.executeUpdate();
}
將「舊記錄軟刪除」與「新增新記錄」拆為兩個獨立 @Transactional 方法,確保 Session 隔離。
// 修改前先 evict,修改後 merge(複雜且易錯)
session.evict(old);
old.setIsNew(IS_NEW_VALUE_NO);
session.merge(old);
原配置使用 BeanNameAutoProxyCreator 攔截 *Service,屬舊式 AOP。建議改用 @EnableTransactionManagement + @Transactional 註解,更清晰控制事務邊界與傳播行為。
時間戳意外更新通常源於「實體映射允許更新」或「資料庫欄位自動更新」。加上 updatable=false / update="false" 並檢查資料庫 DDL,配合 HQL 單欄位更新避免完整實體 dirty check,即可徹底解決。Spring 事務配置現代化亦能減少此類隱性問題。