13 KiB
13 KiB
GridView 銜接的 SqlDataSource 改用 ObjectDataSource
分支:feature/gridview-to-objectdatasource
目標與原則
- 只改以
DataSourceID綁定 SqlDataSource 的 GridView;檔內的 FormView / DataList / DropDownList 之 SqlDataSource 不動。 - 缺的 DAC 建立專屬 DAC;查詢方法只要
SelectPage與SelectCount,不需要 Insert / Update / Delete。 - 沿用既有 ObjectDataSource 工法(參考
fmRpir/RpirDAC):- ObjectDataSource 設定
TypeName、SelectMethod、SelectCountMethod、EnablePaging="true"。 SelectPage前兩個參數為int startRowIndex, int maximumRows(由 ObjectDataSource 自動帶入)。- 動態篩選邏輯由 code-behind 的
SelectCommand字串組裝,改為 DAC 內參數化查詢,code-behind 只設SelectParameters[...].DefaultValue。 - 事件簽章由
SqlDataSourceStatusEventArgs改為ObjectDataSourceStatusEventArgs。
- ObjectDataSource 設定
盤點:GridView ↔ SqlDataSource(共 24 個 / 19 檔,皆在 Web 專案)
| # | 檔案 | GridView ID | SqlDataSource ID | 來源表 | 既有 DAC | 狀態 |
|---|---|---|---|---|---|---|
| 1 | forms/customer/fmBcus.ascx | gvBCUS | dsBCUSGrid | BCUS | 無→BcusDAC | 完成 |
| 2 | forms/customer/fmBpaa.ascx | gvBPAA | dsBPAAGrid | BPAA | BpaaDAC(+Select) | 完成 |
| 3 | forms/customer/fmBpab.ascx | gvBPAB | dsBPABGrid | BPAB | 無→BpabDAC | 完成 |
| 4 | forms/customer/fmBpabBig.ascx | gvBPAB | dsBPABGrid | BPAB | 無→BpabDAC | 完成 |
| 5 | forms/customer/fmBpabSmall.ascx | gvBPAB | dsBPABGrid | BPAB | 無→BpabDAC | 完成 |
| 6 | forms/customer/fmBpja.ascx | gvBPJA | dsBPJAGrid | BPJA | 無→BpjaDAC | 完成 |
| 7 | forms/customer/fmBpjb.ascx | gvBPJB | dsBPJBGrid | BPJB | 無→BpjbDAC | 完成 |
| 8 | forms/customer/fmBprj.ascx | gvBPRJ | dsBPRJGrid | BPRJ | BprjDAC(+Select) | 完成 |
| 9 | forms/customer/fmBprm.ascx | gvBPRM | dsBPRMGrid | BPRM | BprmDAC(+Select) | 完成 |
| 10 | forms/customer/fmRasn.ascx | gvRASN_RAENO | dsRASN_RAENO1 | RASN+BPSNView | RasnDAC(+Select) | 完成 |
| 11 | forms/customer/fmRasn.ascx | gvRASN_RABRH | dsRASN_RABRH1 | RASN+BCLS | RasnDAC(+Select) | 完成 |
| 12 | forms/customer/fmRasn.ascx | RASNGrid | dsRASNGrid | RASN/BPJB/BPSNView | RasnDAC(+Select) | 完成 |
| 13 | forms/customer/fmRpfe.ascx | RPFEGrid | dsRPFEGrid | RPFE | 無→RpfeDAC | 完成 |
| 14 | forms/flow/fmSign.ascx | gvRPAB2 | dsRPAB2 | RPAB | RpabDAC(+Select) | 完成 |
| 15 | forms/flow/fmSign.ascx | gvRPAB | dsRPAB | RPAB | RpabDAC(+Select) | 完成 |
| 16 | forms/summary/fmSalaryCalc.ascx | GridView1 | dsSACD | SACD | 無→SacdDAC | 完成 |
| 17 | forms/worker/fmBpsn.ascx | GridView1 | dsBPSNGrid | BPSNView | BpsnDAC | 進行中 |
| 18 | forms/worker/fmRowk.ascx | gvROWK | dsROWKGrid | ROWK | 無 | 待辦 |
| 19 | forms/worker/fmRpar.ascx | RPARGrid | dsRPARGrid | RPAR | RparDAC | 待辦 |
| 20 | forms/worker/fmRparWorker.ascx | RPABGrid | dsRPAB | RPAB | RpabDAC | 待辦 |
| 21 | forms/worker/fmRparWorker.ascx | RPARGrid | dsRPARGrid | RPAR | RparDAC | 待辦 |
| 22 | forms/worker/fmRsaj.ascx | RSAJGrid | dsRSAJGrid | RSAJ | RsajDAC | 待辦 |
| 23 | forms/worker/fmSendSalaryStrip.ascx | gvSSMS | dsSSMS | SSMS | SsmsDAC | 待辦 |
| 24 | forms/worker/fmSendSalaryStrip.ascx | gvSSMSSum | dsSSMSSum | SSMS | SsmsDAC | 待辦 |
已為 ObjectDataSource(不列入):
fmBPAP(dsBPAP)、fmRparManager(dsRpad)、fmSign內 dsRPAR未簽/已簽/我的/我的督導、fmBpsl、fmBPSE、fmRpir。
驗證樣板:fmBPSN
現況
fmBpsn.ascxGridView1:DataSourceID="dsBPSNGrid"、DataKeyNames="BPENO"、AllowPaging="True",事件OnDataBound / OnPageIndexChanged / OnRowDataBound / OnSelectedIndexChanged。dsBPSNGrid:SelectCommand="SELECT * FROM BPSNView WHERE 1=0"、OnSelected="dsBPSNGrid_Selected"。fmBpsn.ascx.cs的Search()以select/from/where欄位 +AddSearch(...)動態組 SQL 後塞入dsBPSNGrid.SelectCommand。- 篩選條件:BPENO/BPNME/BPENM/BPIDN(LIKE)、BPEDY_TYPE(在職/離職)、BPDPT_TYPE(部門/專案,
PublicVariable.InnerDepartment)、BPSDY/BPEDY 區間、非管理者排除00001/00002/00003、!canSearch時只查自己。 - 另有「依 BPENO 跳到該員工所在頁」邏輯(用
ROWID - 1反查)。
變更內容
Web/App_Code/DAC/BpsnDAC.cs:新增SelectPage(int startRowIndex, int maximumRows, string BPENO, string BPNME, string BPENM, string BPIDN, string BPEDY_TYPE, string BPDPT_TYPE, string BPSDY1, string BPSDY2, string BPEDY1, string BPEDY2, bool canSearch, string userId)SelectCount(同上,去掉 startRowIndex/maximumRows)SelectBPENOIndex(string targetBPENO, 同 SelectCount 參數)(保留自動跳頁)
Web/forms/worker/fmBpsn.ascx:dsBPSNGrid由 SqlDataSource 改為 ObjectDataSource(TypeName="BpsnDAC"、SelectMethod="SelectPage"、SelectCountMethod="SelectCount"、EnablePaging="true")。Web/forms/worker/fmBpsn.ascx.cs:Search()改設 SelectParameters;dsBPSNGrid_Selected改ObjectDataSourceStatusEventArgs;OnInit移除 ConnectionString 設定;移除不再使用的select/from/where欄位。
驗證步驟
aspnet_compiler -v / -p "D:\VS.NET\Thinkyu\Web"(或 VS 開站台)確認 App_Code 編譯無誤。- 手動測試:各篩選條件、在職/離職、部門/專案、分頁、SelectedIndex、無資料 NODATA、由 RPIR 帶 BPENO 進入時自動跳頁。
- 核對 Grid 回傳欄位與舊 SQL 一致(RAPYN/RAPYNX/RABRH/RABRHX/RAJNO/RAJNOX/RACNO/RACNOX/RPCPY)。
效能比較(fmBPSN:舊 SqlDataSource vs 新 ObjectDataSource)
方法
正式資料庫(192.168.1.50:23450)不可達,改用本機 SQL Server LocalDB 建立代表性 schema(BPSNView + RASN/BPAA/BCLS/BPJA/BPJB/RPIR/RPIB 與相關索引)模擬。基準程式為 C# + System.Data.SqlClient,比較:
- 舊作法:SqlDataSource 動態 SQL 全量查詢(GridView 端分頁)。
- 新作法:ObjectDataSource
SelectPage(TOP 10)+SelectCount(PageSize=10)。
取 7 次平均,SET STATISTICS IO 統計 logical reads。此為合成環境、無網路傳輸,絕對值與正式環境不同,比例可作參考。
結果(Filter=全部)
| 指標 | N=20,000 舊 | N=20,000 新(首頁) | N=100,000 舊 | N=100,000 新(首頁) |
|---|---|---|---|---|
| 回傳/繫結列數 | 20,000 | 10 | 100,000 | 10 |
| DB 查詢時間 | 758.0 ms | 123.1 ms(count 122.0 + page 1.1) | 4,289.6 ms | 715.3 ms(count 714.0 + page 1.3) |
| logical reads | 81,662 | 81,863 | 608,191 | 608,443 |
| App 記憶體(DataTable) | ~54.6 MB | ~0 | ~252 MB | ~0 |
- 首頁
SelectPage極快(1.1~1.3 ms、216~267 reads):可用BPENO索引提早結束。 - DB 成本幾乎全落在
SelectCount(與舊全量查詢相同 reads),因為 COUNT 仍需掃描同樣資料。 - 真正的效能來源是 DB→App 的資料量由 N 列降為 10 列,省下網路傳輸與 DataTable/GridView 繫結;網路越慢、欄位越多,實務改善越大。
- 深頁跳轉較差:
ROWID > @START且 START 很大時,無法提早結束,N=100,000 時 page 需 2,351 ms / 2,621,036 reads(約舊作法的 4.3 倍 reads)。此即 fmBPSN「依 BPENO 自動跳頁」可能踩到的情境。
結論
- 一般瀏覽(首頁、逐頁):大幅改善(列數 ~10,000×、記憶體趨近 0、DB 時間約 6×)。
- 深頁跳轉:DB reads 反而增加;後續可評估限制跳頁深度、或改用 keyset 分頁優化。
分頁 SQL 形式比較(N=100,000、PageSize=10、7 次平均)
| 形式 | page@0 ms | page@0 reads | page@deep ms | page@deep reads |
|---|---|---|---|---|
| OLD(全量) | 4,098.0 | 608,191 | 4,053.0 | 608,191 |
TOP(現行 SelectPage) |
7.4 | 273 | 2,166.3 | 2,621,045 |
BETWEEN @START+1 AND @START+@MAX(兩層查詢) |
3.1 | 268 | 2,192.3 | 2,621,037 |
KEYSET:先在窄集合算 ROWID 再 join 明細 |
3.4 | 300 | 48.0 | 6,549 |
BETWEEN與TOP幾乎相同:兩者都必須對「過濾後、已 join 的完整集合」計算ROW_NUMBER();當 offset 很大時仍要處理前面所有資料列,故深頁成本相同。BETWEEN不會改善。KEYSET形式:把ROW_NUMBER()只算在BPSNView(篩選欄位全部屬於 BPSNView),外層再用ROWID BETWEEN取出該頁的 10 個BPENO,之後才 join 明細表。深頁由 2,166 ms / 262 萬 reads 降為 48 ms / 6,549 reads(約 45× 快、400× reads)。- 限制:
KEYSET假設各明細 join 為 1:1(特別是RASN的現職派遣),否則原本「以 join 後結果計數」的語意會不同(同一員工可能多列)。已於 1:1 合成資料驗證TOP與KEYSET取出的頁面完全相同(差異 0 筆);正式環境需先確認RASN現職是否唯一。
最終實作(已套用於 BpsnDAC)
SelectPage改為 KEYSET:先在BPSNView(套用篩選)算ROW_NUMBER()取出該頁BPENO,再 join 明細;WHERE R.ROWID BETWEEN @STARTROWINDEX+1 AND @STARTROWINDEX+@MAXIMUMROWS。SelectCount改為窄集合SELECT COUNT(*) FROM BPSNView A WHERE 1=1 {filter}(與 KEYSET 分頁一致,且不需 join)。SelectBPENOIndex同步改為窄集合。
| 指標(N=100,000、PageSize=10) | 舊 SqlDataSource | 新 KEYSET |
|---|---|---|
| SelectPage 首頁 | 4,098 ms / 608,191 reads(全量) | 3.4 ms / 300 reads |
| SelectPage 深頁 | 4,053 ms / 608,191 reads | 48 ms / 6,549 reads |
| SelectCount(新增) | — | 110 ms / 6,253 reads |
| 首頁總計(count + page) | 4,098 ms / 608,191 reads | ~113 ms / ~6,553 reads |
| 回傳/繫結列數 | 100,000 | 10 |
| App 記憶體 | ~252 MB | ~0 |
改善:首頁總計約 38× 快、93× 少 reads;深頁約 45× 快。原本 count 需 608,176 reads,窄集合後降為 6,253 reads。
待辦清單(Todo)
- 建立分支
feature/gridview-to-objectdatasource - 撰寫本計畫文檔
- fmBPSN:BpsnDAC 新增 SelectPage / SelectCount / SelectBPENOIndex
- fmBPSN:fmBpsn.ascx 改 dsBPSNGrid 為 ObjectDataSource
- fmBPSN:fmBpsn.ascx.cs 調整 Search / Selected / OnInit
- fmBPSN:編譯驗證(aspnet_compiler 通過,產出 App_Code.dll)
- fmBPSN:手動功能驗證(各篩選、分頁、跳頁、NODATA)
- fmBcus:新增 BcusDAC(SelectPage/SelectCount/SelectBCNUMIndex);dsBCUSGrid 改 ObjectDataSource;Search/Selected/DataBound 調整;編譯通過
- 其餘 22 個 Grid 依樣板陸續轉換(逐檔 commit)
變更紀錄
| 日期 | 內容 |
|---|---|
| 2026-09-10 | 建立計畫文檔、盤點清單;開始 fmBPSN 驗證 |
| 2026-09-10 | fmBPSN 轉換完成:BpsnDAC 新增 SelectPage/SelectCount/SelectBPENOIndex;fmBpsn.ascx 改 ObjectDataSource;fmBpsn.ascx.cs 調整 Search/Selected/OnInit;aspnet_compiler 編譯通過 |
| 2026-09-10 | 修復 BpsnDAC.cs 亂碼註解(原檔為 Big5,編輯後毀損),自 git HEAD 還原中文註解,並將檔案統一改存為 UTF-8 with BOM;重新編譯通過 |
| 2026-09-10 | 以 LocalDB 合成資料(2 萬/10 萬筆)基準測試舊 SqlDataSource 與新 ObjectDataSource;結果:首頁列數 ~10,000×、記憶體趨近 0、DB 時間約 6×;深頁跳轉 reads 反而增加 |
| 2026-09-10 | 分頁 SQL 形式比較:BETWEEN 與 TOP 相同(無改善);改採 KEYSET(窄集合算 ROWID 再 join 明細)並將 SelectCount/SelectBPENOIndex 改為窄集合。深頁 2,166ms/262萬 reads → 48ms/6,549 reads;count 714ms/608,176 reads → 110ms/6,253 reads;aspnet_compiler 編譯通過 |
| 2026-09-10 | 轉換 fmBcus(BCUS):新增 BcusDAC;dsBCUSGrid 改 ObjectDataSource;跳回選取列改用 SelectBCNUMIndex;aspnet_compiler 編譯通過 |
| 2026-09-10 | 轉換 fmBpaa(BPAA):BpaaDAC 新增 SelectPage/SelectCount/SelectBPAAIndex;dsBPAAGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpab / fmBpabBig / fmBpabSmall(BPAB):新增 BpabDAC(SelectPage/SelectCount,mode 區分科目/大類/小類);三檔 dsBPABGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpja(BPJA):新增 BpjaDAC(SelectPage/SelectCount/SelectBPJAIndex);dsBPJAGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpjb(BPJB):新增 BpjbDAC(SelectPage/SelectCount);dsBPJBGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBprj(BPRJ):BprjDAC 新增 SelectPage/SelectCount/SelectBPRJIndex(含 LAST_YEAR 排序);dsBPRJGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBprm(BPRM):BprmDAC 新增 SelectPage/SelectCount;dsBPRMGrid 改 ObjectDataSource;dsBPRM_Selected 拆分為 grid/form 兩個事件;編譯通過 |
| 2026-09-10 | 轉換 fmRasn(RASN,3 個 Grid):RasnDAC 新增 SelectPage/SelectCount 及 SelectRAENO/SelectRABRH;三個 datasource 改 ObjectDataSource;Search/Selected 調整;編譯通過 |
| 2026-09-10 | 轉換 fmRpfe(RPFE):新增 RpfeDAC(SelectPage/SelectCount);dsRPFEGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmSign(RPAB):RpabDAC 新增 SelectRPAB;dsRPAB2/dsRPAB 改 ObjectDataSource;dsRPAB2_Selecting 改 ODS 事件;編譯通過 |
| 2026-09-10 | 轉換 fmSalaryCalc(SACD):新增 SacdDAC(SelectByProject);dsSACD 改 ObjectDataSource;編譯通過 |