本篇部落客要描述分頁的常見技術方案,以及在 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 分頁、資料庫分頁。
資料庫分頁(分頁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。這時可能出錯。
希望大夥拍磚。