Files
thinkyu/Doc/GridView改用ObjectDataSource計畫.md
T
sryang 439e941268 feat: fmSendSalaryStrip GridView 改用 ObjectDataSource
- SsmsDAC 新增 SelectSalaryStrip/SelectSalaryStripSum

- dsSSMS 改 ObjectDataSource;dsSSMSSum 移除,footer 改用 SsmsDAC
2026-09-10 11:04:33 +08:00

161 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
## 盤點: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 | 無 | **保留**(使用 WUserControlBase 的 SqlDataSource 框架,需另案) |
| 19 | forms/worker/fmRpar.ascx | RPARGrid | dsRPARGrid | RPAR | RparDAC(+Select) | **完成** |
| 20 | forms/worker/fmRparWorker.ascx | RPABGrid | dsRPAB | RPAB | RpabDAC(+Select) | **完成** |
| 21 | forms/worker/fmRparWorker.ascx | RPARGrid | dsRPARGrid | RPAR | RparDAC(+Select) | **完成** |
| 22 | forms/worker/fmRsaj.ascx | RSAJGrid | dsRSAJGrid | RSAJ | RsajDAC(+Select) | **完成** |
| 23 | forms/worker/fmSendSalaryStrip.ascx | gvSSMS | dsSSMS | SSMS | SsmsDAC(+Select) | **完成** |
| 24 | forms/worker/fmSendSalaryStrip.ascx | gvSSMSSum(已註解) | dsSSMSSum | SSMS | SsmsDAC | **完成**(移除 dsSSMSSumfooter 改用 SsmsDAC |
> 已為 ObjectDataSource(不列入):`fmBPAP`dsBPAP)、`fmRparManager`dsRpad)、`fmSign` 內 dsRPAR未簽/已簽/我的/我的督導、`fmBpsl`、`fmBPSE`、`fmRpir`。
## 驗證樣板:fmBPSN
### 現況
- `fmBpsn.ascx` GridView1`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/BPIDNLIKE)、BPEDY_TYPE(在職/離職)、BPDPT_TYPE(部門/專案,`PublicVariable.InnerDepartment`)、BPSDY/BPEDY 區間、非管理者排除 `00001/00002/00003``!canSearch` 時只查自己。
- 另有「依 BPENO 跳到該員工所在頁」邏輯(用 `ROWID - 1` 反查)。
### 變更內容
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 參數)`(保留自動跳頁)
2. `Web/forms/worker/fmBpsn.ascx``dsBPSNGrid` 由 SqlDataSource 改為 ObjectDataSource`TypeName="BpsnDAC"``SelectMethod="SelectPage"``SelectCountMethod="SelectCount"``EnablePaging="true"`)。
3. `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 mscount 122.0 + page 1.1 | 4,289.6 ms | 715.3 mscount 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.11.3 ms、216267 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
- [x] 建立分支 `feature/gridview-to-objectdatasource`
- [x] 撰寫本計畫文檔
- [x] fmBPSNBpsnDAC 新增 SelectPage / SelectCount / SelectBPENOIndex
- [x] fmBPSNfmBpsn.ascx 改 dsBPSNGrid 為 ObjectDataSource
- [x] fmBPSNfmBpsn.ascx.cs 調整 Search / Selected / OnInit
- [x] fmBPSN:編譯驗證(aspnet_compiler 通過,產出 App_Code.dll
- [ ] fmBPSN:手動功能驗證(各篩選、分頁、跳頁、NODATA)
- [x] fmBcus:新增 BcusDACSelectPage/SelectCount/SelectBCNUMIndex);dsBCUSGrid 改 ObjectDataSourceSearch/Selected/DataBound 調整;編譯通過
- [ ] 其餘 22 個 Grid 依樣板陸續轉換(逐檔 commit)
## 變更紀錄
| 日期 | 內容 |
|---|---|
| 2026-09-10 | 建立計畫文檔、盤點清單;開始 fmBPSN 驗證 |
| 2026-09-10 | fmBPSN 轉換完成:BpsnDAC 新增 SelectPage/SelectCount/SelectBPENOIndexfmBpsn.ascx 改 ObjectDataSourcefmBpsn.ascx.cs 調整 Search/Selected/OnInitaspnet_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 readscount 714ms/608,176 reads → 110ms/6,253 readsaspnet_compiler 編譯通過 |
| 2026-09-10 | 轉換 fmBcusBCUS):新增 BcusDACdsBCUSGrid 改 ObjectDataSource;跳回選取列改用 SelectBCNUMIndexaspnet_compiler 編譯通過 |
| 2026-09-10 | 轉換 fmBpaaBPAA):BpaaDAC 新增 SelectPage/SelectCount/SelectBPAAIndexdsBPAAGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpab / fmBpabBig / fmBpabSmallBPAB):新增 BpabDACSelectPage/SelectCountmode 區分科目/大類/小類);三檔 dsBPABGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpjaBPJA):新增 BpjaDACSelectPage/SelectCount/SelectBPJAIndex);dsBPJAGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBpjbBPJB):新增 BpjbDACSelectPage/SelectCount);dsBPJBGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBprjBPRJ):BprjDAC 新增 SelectPage/SelectCount/SelectBPRJIndex(含 LAST_YEAR 排序);dsBPRJGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmBprmBPRM):BprmDAC 新增 SelectPage/SelectCountdsBPRMGrid 改 ObjectDataSourcedsBPRM_Selected 拆分為 grid/form 兩個事件;編譯通過 |
| 2026-09-10 | 轉換 fmRasnRASN3 個 Grid):RasnDAC 新增 SelectPage/SelectCount 及 SelectRAENO/SelectRABRH;三個 datasource 改 ObjectDataSourceSearch/Selected 調整;編譯通過 |
| 2026-09-10 | 轉換 fmRpfeRPFE):新增 RpfeDACSelectPage/SelectCount);dsRPFEGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmSignRPAB):RpabDAC 新增 SelectRPABdsRPAB2/dsRPAB 改 ObjectDataSourcedsRPAB2_Selecting 改 ODS 事件;編譯通過 |
| 2026-09-10 | 轉換 fmSalaryCalcSACD):新增 SacdDACSelectByProject);dsSACD 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmRparRPAR):RparDAC 新增 SelectPage/SelectCountdsRPARGrid 改 ObjectDataSourceSearch/Selected 調整;編譯通過 |
| 2026-09-10 | 轉換 fmRparWorkerRPAR/RPAB):RparDAC 新增 SelectWorkerPage/SelectWorkerCountRpabDAC 新增 SelectRPABByRPRENdsRPARGrid/dsRPAB 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmRsajRSAJ):RsajDAC 新增 SelectPage/SelectCountdsRSAJGrid 改 ObjectDataSource;編譯通過 |
| 2026-09-10 | 轉換 fmSendSalaryStripSSMS):SsmsDAC 新增 SelectSalaryStrip/SelectSalaryStripSumdsSSMS 改 ObjectDataSource;移除 dsSSMSSumfooter 改用 SsmsDAC);編譯通過 |
| 2026-09-10 | fmRowkROWK)保留未轉換:其 Grid 綁定使用 WUserControlBase 的 Grid_View/GridView_DataSourceSqlDataSource)框架與 override Search,無法直接改用 ObjectDataSource,需另案重構 |