2007-03-22

[Debug] Error from Java Object Reference, Hibernate and Spring MVC

環境介紹
使用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);
// ...
}

沒有留言:

張貼留言