領域模型關係:

重點區域:Project(建設專案)→ ProjectPBS(專案 PBS 清單)→ PBSPropertyValue(PBS 屬性值,對應範本定義的屬性與可選值)。
UI 介面:左側專案清單,點選後右側顯示該專案所有 PBS 及屬性值。

避免 Lazy Load 造成 N+1 遠端呼叫,使用單一 SQL 聯結三張表:
private static readonly string SQL_GET_PBS_BY_PBSTYPE_WITH_PROPERTIES = string.Format(@"
select
{0}, -- PBS 欄位
{1}, -- PBSProperty 欄位 (別名 p)
{2} -- PBSPropertyOptionalValue 欄位 (別名 v)
from PBS pbs
left outer join PBSProperty p on pbs.Id = p.PBSId
left outer join PBSPropertyOptionalValue v on p.Id = v.PBSPropertyId
where pbs.PBSTypeId = '{{0}}'
order by pbs.Id, p.Id",
PBS.GetReadableColumnsSql(),
PBSProperty.GetReadableColumnsSql("p"),
PBSPropertyOptionalValue.GetReadableColumnsSql("v"));
資料層 DataPortal_Fetch 執行 SQL,將 DataTable 透過 ReadFromTable 還原為物件圖(PBS.GetChild_With_Properties)。
問題:聚合 SQL 資料量大,同步等待會凍結 UI。
解法:
ForeAsyncLoader 執行聚合 SQL 載入所有屬性值。EventHandler projectPBSView_DataChanged = (sender, e) =>
{
var project = view.CurrentObject as Project;
if (project != null) project.PBSPropertyValuesLoader.BeginLoading();
};
projectPBSView.DataChanged += projectPBSView_DataChanged;
[NonSerialized, NotUndoable]
private ForeAsyncLoader _PBSPropertyValuesLoader;
public ForeAsyncLoader PBSPropertyValuesLoader
{
get
{
if (_PBSPropertyValuesLoader == null)
_PBSPropertyValuesLoader = new ForeAsyncLoader(this.LoadPBSPropertyValues);
return _PBSPropertyValuesLoader;
}
}
private static PropertyInfo<PBSPropertys> PBSPropertysProperty =
RegisterProperty(new PropertyInfo<PBSPropertys>("PBSPropertys"));
[Association]
public PBSPropertys PBSPropertys
{
get { return this.GetLazyChildren(PBSPropertysProperty, PBSPropertys.NewChild, PBSPropertys.Get); }
}
因無「唯一實體」機制,背景載入的物件與 UI 綁定物件為不同實例。若使用者在載入完成前已編輯某 PBS 屬性,單純覆蓋會導致資料遺失。
解法:載入每筆 PBS 屬性前,先判斷該 PBS 是否已有本地編輯(PropertyValuesLoaded()),若有則合併(LoadPropertyValues),否則直接替換。
private bool _PBSPropertyValuesLoaded;
private void LoadPBSPropertyValues()
{
if (this._PBSPropertyValuesLoaded) return;
lock (this)
{
bool pbssLoaded = this.FieldManager.FieldExists(ProjectPBSsProperty);
ProjectPBS[] projectPBSCache = null;
if (pbssLoaded)
{
projectPBSCache = this.ProjectPBSs.ToArray();
this._PBSPropertyValuesLoaded = projectPBSCache.All(pp => pp.PropertyValuesLoaded());
if (this._PBSPropertyValuesLoaded) return;
}
var oldProjectPBSs = ProjectPBSs.GetListByProject_With_PropertyValues(this);
if (pbssLoaded)
{
foreach (var pbs in projectPBSCache)
pbs.LoadPropertyValues(oldProjectPBSs); // 合併邏輯
}
else
{
this.LoadProperty(ProjectPBSsProperty, oldProjectPBSs);
}
this._PBSPropertyValuesLoaded = true;
}
}
lock(this) 防止多重非同步載入競爭。
實施後使用者反饋:「軟體快多了,早這樣做就好。」
下一步重構目標:簡化聚合載入 API,減少手寫 SQL、方法定義樣板碼,但不想重造 ORM 輪子。
本系列至此告一段落。