使用Hibernate與Spring MVC,主要物件有三:Document、Status與StatusLog。
每次Document的status變更時,必須同時寫一筆log檔到StatusLog,而StatusLog也會紀錄status作為歷史紀錄使用。
程式如下:
// Domain objects public class Document { private Status status; private Collection logs = new ArrayList(); // ... } public class Status { // ... } public class StatusLog { private Status status; public StatusLog() { } public StatusLog(Status status) { this.status = status; } // ... } // Spring Controller public class DocumentFormCtrl extends CommonFormCtrl { // ... @Override protected Object formBackingObject(HttpServletRequest request) throws ServletRequestBindingException { Long id = ServletRequestUtils.getRequiredLongParameter(request, "id"); Document cmd = this.documentService.getDocument(id); // ... return cmd; } @Override protected void onBind(HttpServletRequest request, Object command, BindException errors) throws ServletRequestBindingException { Document cmd = (Document) command; // ... cmd.getStatus().setId(statusId); // 呆會看這邊 // 同時寫一筆log檔到StatusLog cmd.getLogs.add(new StatusLog(...)); // ... } }狀況描述
背景:假設StatusLog裡已經有三筆歷史資料,分別為Status A、Status B與Status C,所以Document裡的status也為Status C。
執行:加入Status D到Document裡。
預期:StatusLog有四筆歷史資料,分別為Status A、Status B、Status C與Status D,Document裡的status也為Status D。
結果:StatusLog有四筆歷史資料,分別為Status A、Status B、Status D與Status D,Document裡的status也為Status D。
問題所在
為什麼既有資料會變?我又沒改它,它怎會變?經過測試後發現,兇手應該是Java Object Reference、Hibernate與Spring MVC三人合作無間,加上本人我參一腳造成的。
先從Hibernate看,Hibernate在載入Document時,也同時載入Status C,以及三筆StatusLog,因為第三筆StatusLog的status也為Status C,所以第三筆StatusLog的status是從Hibernate session的cache中取出來的,不同於Document的status是從資料庫取出來的,所以Document的status與第三筆StatusLog的status是reference到Java heap裡的同一筆物件,所以改了其中一筆自然會影響另一筆,反之亦然。
接下來看Spring MVC,controller在binding parameter時,是將statusId直接塞到Document的status裡,並非塞一筆全新的Status物件,所以在此同時第三筆StatusLog的status的id也被改掉了。
解決方法
所以controller在binding parameter時,得塞一筆全新的Status物件,不可以拿既有物件來改!
@Override protected void onBind(HttpServletRequest request, Object command, BindException errors) throws ServletRequestBindingException { Document cmd = (Document) command; // ... // 詭異絕倫的bug,這裡一定要設為新的物件,不然在歷史紀錄裡共用原物件的都會受到影響 cmd.setStatus(new Status()); cmd.getStatus().setId(statusId); // ... }
沒有留言:
張貼留言