在企業級應用開發中,面對百萬級資料量的分頁查詢是常見且具挑戰性的任務。不同資料庫 (Oracle、SQL Server、DB2、MySQL、PostgreSQL) 的分頁語法差異極大,早期開發者往往為每種資料庫撰寫專屬邏輯,維護成本高且易出錯。本文介紹一個通用分頁元件設計,透過策略模式封裝各資料庫分頁語法,實現「寫一次,跨資料庫部署」的目標。
public interface IPagingStrategy
{
///
/// 產生分頁查詢 SQL
///
/// 原始查詢 SQL (不含 ORDER BY)
/// 起始列索引 (0-based)
每頁筆數
排序欄位
/// 分頁 SQL
string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy);
}
public class Oracle12cPagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
return $@"
SELECT * FROM ({originalSql})
ORDER BY {orderBy}
OFFSET {startRowIndex} ROWS FETCH NEXT {maximumRows} ROWS ONLY";
}
}
public class OracleLegacyPagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
int endRow = startRowIndex + maximumRows;
return $@"
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (
SELECT * FROM ({originalSql}) ORDER BY {orderBy}
) a WHERE ROWNUM <= {endRow}
) WHERE rn > {startRowIndex}";
}
}
public class SqlServer2012PagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
return $@"
SELECT * FROM ({originalSql}) AS T
ORDER BY {orderBy}
OFFSET {startRowIndex} ROWS FETCH NEXT {maximumRows} ROWS ONLY";
}
}
public class SqlServerLegacyPagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
int endRow = startRowIndex + maximumRows;
return $@"
SELECT * FROM (
SELECT ROW_NUMBER() OVER (ORDER BY {orderBy}) AS RowNum, *
FROM ({originalSql}) AS T
) AS Paged
WHERE RowNum > {startRowIndex} AND RowNum <= {endRow}";
}
}
public class MySqlPagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
return $@"
SELECT * FROM ({originalSql}) AS T
ORDER BY {orderBy}
LIMIT {maximumRows} OFFSET {startRowIndex}";
}
}
public class DB2PagingStrategy : IPagingStrategy
{
public string GetPagingSql(string originalSql, int startRowIndex, int maximumRows, string orderBy)
{
// DB2 10.5+ 支援 OFFSET FETCH
return $@"
SELECT * FROM ({originalSql}) AS T
ORDER BY {orderBy}
OFFSET {startRowIndex} ROWS FETCH FIRST {maximumRows} ROWS ONLY";
}
}
public static class PagingStrategyFactory
{
public static IPagingStrategy Create(string providerName)
{
switch (providerName.ToLower())
{
case "system.data.oracleclient":
case "oracle.dataaccess.client":
// 可進一步檢測 Oracle 版本決定新舊語法
return new OracleLegacyPagingStrategy(); // 保守策略
case "system.data.sqlclient":
case "microsoft.data.sqlclient":
// 檢測 SQL Server 版本
return new SqlServerLegacyPagingStrategy(); // 保守策略
case "mysql.data.mysqlclient":
return new MySqlPagingStrategy();
case "ibm.data.db2":
return new DB2PagingStrategy();
case "npgsql":
return new MySqlPagingStrategy(); // PostgreSQL 語法相容 MySQL
default:
throw new NotSupportedException($"不支援的資料庫提供者: {providerName}");
}
}
}
public class PagingDataAccess
{
private readonly IPagingStrategy _strategy;
private readonly string _connectionString;
private readonly DbProviderFactory _factory;
public PagingDataAccess(string providerName, string connectionString)
{
_factory = DbProviderFactories.GetFactory(providerName);
_connectionString = connectionString;
_strategy = PagingStrategyFactory.Create(providerName);
}
///
/// 取得分頁資料
///
public DataTable GetPagedData(string baseSql, int startRowIndex, int maximumRows, string sortExpression)
{
string pagingSql = _strategy.GetPagingSql(baseSql, startRowIndex, maximumRows, sortExpression);
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
using (DbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = pagingSql;
using (DbDataAdapter adapter = _factory.CreateDataAdapter())
{
adapter.SelectCommand = cmd;
DataTable dt = new DataTable();
adapter.Fill(dt);
return dt;
}
}
}
}
///
/// 取得總筆數 (用於分頁控制項計算總頁數)
///
public int GetTotalCount(string baseSql)
{
string countSql = $"SELECT COUNT(*) FROM ({baseSql}) AS CountTable";
using (DbConnection conn = _factory.CreateConnection())
{
conn.ConnectionString = _connectionString;
using (DbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = countSql;
conn.Open();
return Convert.ToInt32(cmd.ExecuteScalar());
}
}
}
}
<asp:ObjectDataSource
ID="odsPaging"
runat="server"
TypeName="Demo.PagingDataAccess"
SelectMethod="GetPagedData"
SelectCountMethod="GetTotalCount"
EnablePaging="true"
StartRowIndexParameterName="startRowIndex"
MaximumRowsParameterName="maximumRows"
SortParameterName="sortExpression">
<SelectParameters>
<asp:Parameter Name="baseSql" Type="String" DefaultValue="SELECT Id, Name, Email, CreateDate FROM Users WHERE IsActive=1" />
<asp:Parameter Name="providerName" Type="String" DefaultValue="System.Data.SqlClient" />
<asp:Parameter Name="connectionString" Type="String" DefaultValue="<%$ ConnectionStrings:DefaultConnection %>" />
</SelectParameters>
</asp:ObjectDataSource>
<asp:GridView
ID="gvData"
runat="server"
DataSourceID="odsPaging"
AllowPaging="true"
AllowSorting="true"
PageSize="20"
AutoGenerateColumns="false"
CssClass="table table-striped">
<Columns>
<asp:BoundField DataField="Id" HeaderText="編號" SortExpression="Id" />
<asp:BoundField DataField="Name" HeaderText="姓名" SortExpression="Name" />
<asp:BoundField DataField="Email" HeaderText="信箱" SortExpression="Email" />
<asp:BoundField DataField="CreateDate" HeaderText="建立日期" SortExpression="CreateDate" DataFormatString="{0:yyyy/MM/dd}" />
</Columns>
<PagerSettings Mode="NumericFirstLast" PageButtonCount="10" />
<PagerStyle CssClass="pagination" />
</asp:GridView>
ORDER BY 欄位有索引,複合索引包含查詢條件欄位async/await 避免阻塞執行緒測試環境:VS2008 + ASP.NET 2.0/3.5 + Oracle 9i,測試資料 41 萬筆,網路頻寬約 100KB/s。
ICachingStrategy 支援 Redis/MemoryCache 快取分頁結果IExportStrategy 支援 Excel/PDF/CSV 匯出Dynamic LINQ 實現動態查詢條件建構透過策略模式與工廠模式封裝資料庫分頁語法差異,配合 ADO.NET DbProviderFactory 實現資料庫無關性,構建出高復用、易維護、跨平台的萬級資料分頁元件。此架構不僅解決當前多資料庫支援需求,更為未來遷移雲端資料庫、微服務化奠定堅實基礎。