iDempiere

iDempiere 開發者的「三明治」防禦術:如何阻止欄位被核心代碼偷偷抹除?

2026-04-20 · 3 分鐘 · Ray Lee (System Analyst)

🥪 iDempiere 開發者的「三明治」防禦術:如何阻止欄位被核心代碼偷偷抹除?

發佈日期: 2026-04-19
標籤: #iDempiere #OSGi #插件開發 #Java #開發技巧


😱 遇見了「消失的數據」

在開發 iDempiere 的 Kanban 插件時,我們遇到了一個超級詭異的 Bug:我們明明在程式碼裡設定了 `R_Request` 的 `EndTime`(結束時間),也確實呼叫了 `save()`,但每次存檔後,那個欄位就像被施了「消失術」一樣,自動變回了 `NULL`。

原本以為是自己寫錯了,結果檢查了半天,發現罪魁敗鞍竟然不是我們。


🕵️‍♂️ 真凶竟然是… `RequestEventHandler`

我們檢查了 `MRequest.beforeSave()`,一切正常。但我們忽略了 iDempiere 核心裡的一個 OSGi 事件處理器:`RequestEventHandler`。

這個傢伙非常「熱心」,它會監聽 `R_Request` 的所有變動事件。最致命的是,它在執行 `beforeSaveRequest()` 時,為了清理暫存欄位,會無條件地執行這行代碼:

r.setEndTime(null); // ← 砰!你的 EndTime 在這裡被抹除了

而且它執行時機非常巧妙,是在 `beforeSave()` 之後、SQL 寫入資料庫之前。這意味著,即便你在 `beforeSave()` 裡寫得再好,只要它一出手,你的數據就沒了。


🥪 絕招:Model Sandwich Pattern(模型三明治模式)

既然我們不能去修改 iDempiere 的核心代碼(那是大忌!),那我們只能用「三明治」的方式,把數據夾在中間保護起來。

這個模式的核心思想很簡單:在「麵包」的一層把值藏起來,等風暴過去後,再從另一層把值拿出來還原。

🥪 三明治的結構如下:

1. 第一層麵包 (`beforeSave`):在 `RequestEventHandler` 搗亂之前,先把 `EndTime` 偷偷存進 PO 的屬性(Attribute)裡。

2. 中間的內餡 (核心事件):`RequestEventHandler` 照常執行,把資料庫裡的 `EndTime` 設為 `NULL`。

3. 第二層麵包 (`afterSave`):趁著 SQL 已經寫入完成,我們立刻使用 `DB.executeUpdateEx()` 直接對資料庫進行一次補救性的 `UPDATE`,把藏好的值還原回去!

🛠️ 實作邏輯

我們需要自定義一個 `MRequestKanban` 類別,並透過 `IModelFactory` 讓系統使用它:

@Override

protected boolean beforeSave(boolean newRecord) {

// 1. 趁它還沒抹除,先把 EndTime 藏到 Attribute 裡

Object endTime = get_Value("EndTime");

if (endTime != null) {

set_Attribute("Ninniku_OriginalEndTime", endTime);

}

return super.beforeSave(newRecord);

}

@Override

protected boolean afterSave(boolean newRecord, boolean success) {

boolean ok = super.afterSave(newRecord, success);

if (success) {

Object stashedEndTime = get_Attribute("Ninniku_OriginalEndTime");

if (stashedEndTime != null) {

// 2. 趁 SQL 剛跑完,直接用 SQL 補救,繞過 PO 生命週期,避免無限迴圈

DB.executeUpdateEx(

"UPDATE R_Request SET EndTime=? WHERE R_依據_ID=?",

new Object[]{stashedEndTime, getR_Request_ID()},

get_TrxName()

);

set_ValueNoCheck("EndTime", stashedEndTime);

}

}

return ok;

}

> ⚠️ 為什麼不用 `saveEx()`?

> 如果你在 `afterSave` 裡面又呼叫 `saveEx()`,會導致 `beforeSave` -> `afterSave` -> `beforeSave` 的無窮迴圈,直接讓你的伺服器當機。所以,直接用 SQL 補救才是最乾脆、最安全的做法!


💡 總結

這個「三明治模式」不只可以用在 `EndTime`,只要遇到任何「核心代碼會隨意重設欄位」的情況,你都可以用這個方法來保護你的數據。

重點筆記:

  • 隱藏:利用 `set_Attribute` 暫存。
  • 繞過:使用 `DB.executeUpdateEx` 避開生命週期迴圈。
  • 同步:最後記得用 `set_ValueNoCheck` 讓記憶體中的物件與資料庫保持一致。
  • 開發 iDempiere 插件時,遇到這種「看不見的敵人」不要慌,用三明治夾住它就對了!🥪💪


    本文首發於 [ninniku.tw](https://www.ninniku.tw)

    Ray Lee (System Analyst)
    作者 Ray Lee (System Analyst)

    iDempeire ERP Contributor, 經濟部中小企業處財務管理顧問 李寶瑞