# 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 | 無 | 待辦 | | 2 | forms/customer/fmBpaa.ascx | gvBPAA | dsBPAAGrid | BPAA | BpaaDAC(無 Select) | 待辦 | | 3 | forms/customer/fmBpab.ascx | gvBPAB | dsBPABGrid | BPAB | 無 | 待辦 | | 4 | forms/customer/fmBpabBig.ascx | gvBPAB | dsBPABGrid | BPAB | 無 | 待辦 | | 5 | forms/customer/fmBpabSmall.ascx | gvBPAB | dsBPABGrid | BPAB | 無 | 待辦 | | 6 | forms/customer/fmBpja.ascx | gvBPJA | dsBPJAGrid | BPJA | 無 | 待辦 | | 7 | forms/customer/fmBpjb.ascx | gvBPJB | dsBPJBGrid | BPJB | 無 | 待辦 | | 8 | forms/customer/fmBprj.ascx | gvBPRJ | dsBPRJGrid | BPRJ | BprjDAC | 待辦 | | 9 | forms/customer/fmBprm.ascx | gvBPRM | dsBPRMGrid | BPRM | BprmDAC | 待辦 | | 10 | forms/customer/fmRasn.ascx | gvRASN_RAENO | dsRASN_RAENO1 | RASN+BPSNView | RasnDAC | 待辦 | | 11 | forms/customer/fmRasn.ascx | gvRASN_RABRH | dsRASN_RABRH1 | RASN+BCLS | RasnDAC | 待辦 | | 12 | forms/customer/fmRasn.ascx | RASNGrid | dsRASNGrid | RASN/BPJB/BPSNView | RasnDAC | 待辦 | | 13 | forms/customer/fmRpfe.ascx | RPFEGrid | dsRPFEGrid | RPFE | 無 | 待辦 | | 14 | forms/flow/fmSign.ascx | gvRPAB2 | dsRPAB2 | RPAB | RpabDAC | 待辦 | | 15 | forms/flow/fmSign.ascx | gvRPAB | dsRPAB | RPAB | RpabDAC | 待辦 | | 16 | forms/summary/fmSalaryCalc.ascx | GridView1 | dsSACD | SACD | 無 | 待辦 | | 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.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/BPIDN(LIKE)、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 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) - [x] 建立分支 `feature/gridview-to-objectdatasource` - [x] 撰寫本計畫文檔 - [x] fmBPSN:BpsnDAC 新增 SelectPage / SelectCount / SelectBPENOIndex - [x] fmBPSN:fmBpsn.ascx 改 dsBPSNGrid 為 ObjectDataSource - [x] fmBPSN:fmBpsn.ascx.cs 調整 Search / Selected / OnInit - [x] fmBPSN:編譯驗證(aspnet_compiler 通過,產出 App_Code.dll) - [ ] fmBPSN:手動功能驗證(各篩選、分頁、跳頁、NODATA) - [ ] 其餘 23 個 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 編譯通過 |