本篇博客主要描述分頁的常見技術方案,以及在 OEA 框架中的分頁的應用及實現原理。
分頁的幾種方案
分頁是解決大數據量顯示的有效方法。根據分頁技術應用的位置不同,大致可以把分頁分為以下幾種:
- 界面層分頁
界面層的分頁,類似於界面的虛擬化技術,是只顯示需要的數據的一種技術。OEA 的 WPF 界面中目前已經實現了 UI 虛擬化,所以不再實現界面層分頁。
優點:
* 簡單。許多控件都支持在界面層直接進行分頁。
* 換頁時,響應快。(在 C/S 結構下使用這種方案,數據都已經到達客戶端,所以在分頁時不需要額外的數據查詢,響應速度較快。)
缺點:
* 不用於太大的數據分頁。由於沒有減少網絡傳輸,首次加載時較慢,需要把所有數據都傳輸到客戶端。
- 實體層分頁
在實體層進行分頁操作的方案,很少會被使用。它是把查詢出來的數據,在服務器端都轉換為實體,然后再找到具體頁的實體數據,其它的數據則直接丟棄。
優點:
* 減少了首次的網絡傳輸,對於客戶端而言,調用的是分頁的 API。
* 簡單。
* 通用性強,與數據庫無關,方案可以跨多種數據庫。
* 統計總行數不需要發起二次查詢。
缺點:
* 占用內存,依然不能用於太大的數據分頁。
- 數據層分頁
這種方案一般使用 IDataReader 實現。查詢的 SQL 依然是查詢所有的數據,但是在對查詢出的 IDataReader 進行遍歷讀取每一行時,只讀取對應頁的數據,其它頁的數據則忽略。同時,遍歷到記錄集的最后一行,即可獲得數據的總行數。
優點:
* 不占用大量內存。只把需要的數據讀取到內存中。
* 簡單。
* 通用性強,與數據庫無關,方案可以跨多種數據庫。
* 統計總行數不需要發起二次查詢。
缺點:
* 查詢的 SQL 會查詢很大的一張表。遍歷依然需要耗費一定的時間。
- 數據庫分頁
分頁的最終方案,自然是在數據庫中進行分頁。這也是大多數情況會選用的方案。
優點:
* 性能最好。速度快、占用內存小。
* 統計行數時,往往需要重新發起查詢。
缺點:
* 對於框架開發而言,要生成分頁相關的 SQL,較麻煩。
* 方案與特定數據庫相關。通用性低。
雖然提到了這幾種不同層面的分頁方案。但是對應應用開發而言,數據庫的分頁是最常用的。只是在做 OEA 框架開發時,由於要支持多種數據庫,所以需要在合適時采用不同的方案。同時,也不會考慮使用存儲過程來輔助分頁。
OEA 分頁 - 應用層接口
在說明 OEA 的分頁前。先介紹一個 PagingInfo 類型(老版本中,該類名為 PagerInfo),這關系到整個分頁方案的接口設計:
圖1 位於 Common(原 hxy)程序集中的 PagingInfo 類型
圖2 PagingInfo 類型接口
在查詢數據時,我們指定了查詢的具體頁碼 PageIndex、一頁所含數據行數 PageSize,就可以把該頁的數據顯示在界面上了。但是,在分頁時,往往要在界面中顯示一個分頁腳,用於顯示當前頁號、所有頁數。所以在進行查詢的同時,往往還需要對結果集中所有數據的總行數進行統計,並把之與查詢出的實體列表數據一同返回。所以,我為 PagingInfo 添加了額外的兩個屬性,IsNeedCount、TotalCount,當 IsNeedCount 被設置為真時,框架在數據層進行查詢時,會把統計出來的總行數賦值給 TotalCount。
OEA 分頁 - 使用方法
下面以分頁查詢所有數據為例,簡單說明如何使用分頁查詢。先是應用層使用的代碼:
應用層需要構造 PagingInfo,並指定需要統計行數。查詢后,直接使用 PagingInfo.TotalCount。(這種接口方案從 06 年使用至今,比較好用。)
下面是 Repository 類型上的公有接口:
最后,再實現該查詢對應的數據層即可:
可以看到,在數據訪問層的 ORM 框架中,主要是在 IQuery 條件類型上添加了一個 Paging 方法。使用這個方法指定了 PagingInfo 后,即按給定的分頁信息分頁查詢實體數據了。
OEA 中的數據層分頁實現
OEA 中用到的分頁有:界面層分頁、DataReader 分頁、數據庫分頁。
- 界面層分頁
其實在 OEA 中就是 UI 虛擬化。相關內容,可以查看《OEA 中 WPF 樹型表格虛擬化設計方案》 及 《 精通 WPF UI Virtualization》。
- 數據庫分頁(分頁SQL)
目前,OEA 已經支持了 SqlServer 2005+、Oracle 10+、SqlCE4+,但是框架的設計目標則是應對所有數據庫(接下來很可能需要對 MySql 進行支持)。這三種數據庫中,OEA 只支持前兩種大型數據庫的數據庫分頁,主要是生成分頁 SQL 進行查詢。
經過對比、挑選,我選用了一種可以在 SqlServer、Oracle 上的一種通用方案,即使用 RowNumber。例如,如果一個 SQL 查詢是:
select ...... from ...... order by xxxx asc, yyyy desc
,則只需要把它轉換為以下格式就行了:
select * from (select ......, row_number() over(order by xxxx asc, yyyy desc) _rowNumber from ......) x where x._rowNumber<10 and x._rowNumber>5 。
同時,當需要統計總行數時,數據層會生成 SELECT COUNT(0) FROM ...... 的 SQL 語句重新進行查詢,並把結果賦值給 PagingInfo.TotalCount,以及 EntityList.TotalCount。
在 SQLCE 中,並不支持 rowNumber 函數。所以只能考慮使用 NOT IN 的 SQL 方案。其實在OEA中,鑒於實現 NOT IN 方案比較麻煩,所以決定暫時使用 DataReader 完成 SQLCE 的內存分頁。
- DataReader 內存分頁
提供 DataReader 方案主要是簡單、同時還能與數據庫無關,解決跨庫問題。主要邏輯代碼如下:
/// <summary>
///使用 IDataReader 的內存分頁讀取方案。
///
///注意!!!
/// 此方法中會釋放 Reader。外層不能再用 Using。
/// </summary>
/// <param name="reader"></param>
/// <param name="rowReader">每一行數據,會調用此方法進行調取。</param>
/// <param name="pagingInfo">分頁信息。如果這個參數不為空,則使用其中描述的分頁規則進行內存分頁查詢。</param>
public static void MemoryPaging(IDataReader reader, Action<IDataReader> rowReader, PagingInfo pagingInfo = null)
{
bool isPaging = pagingInfo != null;
bool needCount = isPaging && pagingInfo.IsNeedCount;
int totalCount = 0;
int startRow = 1;//從一開始的行號
int endRow = int.MaxValue;
if (isPaging)
{
startRow = pagingInfo.PageSize * pagingInfo.PageIndex + 1;
endRow = startRow + pagingInfo.PageSize - 1;
}
using (reader)
{
while (reader.Read())
{
totalCount++;
if (totalCount >= startRow)
{
if (totalCount <= endRow)
{
rowReader(reader);
}
else
{
//如果已經超出該頁,而且需要統計行數,則直接快速循環到最后。
if (needCount)
{
while (reader.Read()) { totalCount++; }
break;
}
}
}
}
}
if (needCount)
{
pagingInfo.TotalCount = totalCount;
}
}
通用,又簡單。
待改進點
目前實現上,可能存在的缺陷是:
- 對分頁 SQL 的轉換不支持復雜的嵌套 SQL。這時可能出錯。
希望大伙拍磚。