J2EE中的分頁

分頁是一種最簡單且廣泛使用的方法,可以把大數據集分成小的數據塊。它是Web站點設計的一個核心部分,包括UI,即客戶端(管理屏幕顯示的內容)和服務器端(高效處理大結果集,防止資源的大量消耗及服務的時延)。

分頁機制需要基於以下兩個條件:
1.) 屏幕顯示的信息是受限的。
2.) 資源配置-數據庫連接數和內存的使用量。

分頁機制有兩種方式:基於緩存的和基於查詢的。
基於緩存:把數據庫中查詢的結果存儲起來,可以是HTTP Session、Stateful Session Bean或自定義的一個緩存機制。下次請求產生的頁面數據就從緩存中而不是數據庫中取得,對於少量的數據和重複的查詢,將非常有用。缺點就是當結果集很大時,會佔用大量的內存,長時間的進行查詢,進而連接超時,長時間佔據數據庫連接和session資源。
基於查詢:不使用緩存,直接從數據庫中取得所需的數據,但請求的響應時間將變長。
所以,大多數情況是混合使用兩種方法,設定緩存的大小,維護少量的緩存數據,使用數據連接池,利用高效的查詢得到數據。

基於查詢的分頁策略
關鍵是隻從數據庫取得是頁面所需的數據。最簡單的方法是把所以的結果都取出來,在迭代輸出到頁面,或利用JDBC取得特定的行,例如JDBC的ResultSet::absolute()。
要注意的就是,雖然在一段連續的數據中某些多餘的數據是不需要的,但DBMS必須在一個臨時區域存放這個集合,爲了能讓遊標移動到指定的開始行。避免這種情況可以使用WHERE從句,把你不需要的數據明確的排除,這就要求你從前/後頁的請求中附帶某些信息:
    * 頁面指向(Paging direction)-前進、後退
    * 查詢條件(Search criteria)
    * 目標頁面(The target page)-只對於請求頁面返回需要的行
    * 頁面大小(Page size)-每頁顯示多少行

頁面指向指可以向前或向後訪問結果數據。緩存能提供向後的功能,把已經得到的數據存儲起來,但它只針對於靜態數據。如果你的數據是動態的,那你緩存中可能沒有先前頁面的數據,只能用另外的查詢去取得了。對於不使用緩存的分頁,要用到ORDER BY和WHERE 從句,對一行或多行使用ORDER BY產生向前(ASC)或向後(DESC)的功能。
通常一個頁面由一對開始和結束的row-id構成。row-id可以是一個主鍵,也可能是由主鍵和其它的列組成。對於下一頁,調整查詢語句取得所有row-id比當前頁的結束row-id大的行;對於上一頁,取得所有row-id比當前頁的開始row-id小的行。這對於單列的查詢能完成的很好,但在大多數情緒下沒這麼簡單,row-id由多列構成,混合了ACS和DESC ORDER BY從句。所以這就要在WHERE從句中包含所有的ORDER BY的列,把所有的row-id的列包含在ORDER BY中,要改變的只是ORDER的順序。

基於row-id的分頁方式
舉一個最簡單的查詢例子:foo表有三列,col1 (VARCHAR)、col2 (INTEGER)和col3 (TIMESTAMP)。
得到第一頁數據的SQL語句:SELECT col1,col2,col3 FROM  foo WHERE col1=? ORDER BY col2 ASC ,col3 DESC;
在這個例子中,查許條件由用戶提供給col1,row-id由col2和col3構成,分頁由col2升序和col3降序來控制。
JDBC給我們提供了一個限制查詢行數的方法:Statement::setMaxRows(int max),一個性能上更好的方法是:Statement::setFetchSize(int rows)。
得到下一頁的SQL語句:SELECT col1,col2,col3 FROM  foo WHERE col1=? AND ( (col2 >  ?  )  OR  (col2 = ?  AND col3 <  ?  )  )  ORDER BY  col2 ASC ,col3 DESC;
得到前一頁的SQL語句:SELECT col1,col2,col3 FROM  foo WHERE   col1=?  AND ( (col2 <  ?  )  OR  (col2 = ?  AND col3 >  ?  )  )  ORDER BY  col2 DESC ,col3 ASC;

基於查詢的實踐
一旦確定使用基於查詢的分頁機制,就開始動手創建一個通用的、可複用的分頁組件,以應付各種類型的查詢。這就要求提供一個清晰的接口,一個簡單API,封裝底層的分頁算法的分頁框架(framework)。它可以處理數據的取得、傳輸、row-id和差數的傳遞、查詢操作、查詢條件差數的替換。
建立這個框架可以採用配置驅動,在一個配置文件中預先定義好各種信息,例如 row-id, ORDER BY列。執行查詢時,調用者是不需要涉及SQL語法的,只要定義一個你所需的view。
view是一個聯繫數據庫表和視圖載體。一個page view包含分頁所需的信息(像row-id)。在.properties文件中配置page view,定義頁面的指向(ORDER BY)和row-id。
下面的步驟是創建一個分頁組件的過程,所有的源代碼點擊
這裏下載。

第一步.創建/配置你的page view
# Example view
example.view=foo
example.pagesize=5
example.where=col1=?
example.rowids=col2 ASC,col3 DESC

第二步.創建一個類,去表述你的view-PageDefn
你可以從配置文件中裝載它,或自己創建它。配置文件當然是首選,調用者不需要知道view的內部構造或表結構。你的PageInfn包含了所有產生SQL語句的細節:

public class PageDefn extends ViewDefn {

 
public interface PageAction 
{
     
public static final int FIRST = 0
;
     
public static final int NEXT = 1
;
     
public static final int PREVIOUS = 2
;
     
public static final int CURRENT = 3
;
   }


    
protected int pageSize;
    
protected
 ColumnDesc[] rowIds;

 
public PageDefn() 
{
      super();
      rowIds 
= null
;
      pageSize 
= 50
;
    }


 
public PageDefn(String viewname) {
      super(viewname);
      rowIds 
= null
;
      pageSize 
= 50
;
   }

 
}

第三步.創建一個DAO(date access object)
創建一個起特殊用途PagingDAO,支持參數替代和PerparedStatement。Prepared statements比一般的查詢速度更快,因爲它能被DBMS預編譯。你的PagingDAO執行分頁查詢和結果的處理,實際的SQL構造交給了PageDefn。這種分離的關係允許你對PageDefn進行擴展,它的子類可以支持更多優秀的查詢構造而不依賴DAO。
當查詢執行結束,確認你的DAO關閉了所有的數據庫資源(ResultSets、Statements和Connections)。把你的關閉代碼放到FINALLY子句中,確保有異常拋出是也可以關閉資源。

第四步.創建PageContext和PageCommand
PageCommand類可以封裝你的頁面請求和放置結果,客戶端servlet和action會使用PageCommand作用於framework,但必須提供如下方法:
    * 指定目標view(PageDefn)
    * 提供可選的查詢條件(query parameters) 
    * 指定頁面action(FIRST, CURRENT, NEXT, or BACK) 
    * 訪問頁面結果

另外,你的PageCommand應該封裝所有實現分頁所需的請求信息。創建一個context對象放置請求action和結果,並封裝了分頁信息:

public class PageContext implements Serializable {

 
protected Object page; // results

 protected Object firstEntry; //  first row ID 
 protected Object lastEntry; // last row id
 protected int action; // paging action
 protected PageContext prevContext; // previous context state

 
public PageContext() {
  
this.page = new Object[0
];
  
this.firstEntry = null
;
  
this.lastEntry = null
;
  
this.action =
 PageDefn.PageAction.FIRST;
 }


 
public Object[] getPage() {
  
return
 ((Collection) page).toArray();
 }


 
public void setPage(Object page) {
  
this.page =
 page;
 }


 
public int getAction() {
  
return
 action;
 }


 
public void setAction(int action) {
  
this.action =
 action;
 }

}

確保你的PageContext對象是可序列化的,它可用於傳輸。創建PageCommand封裝你的頁面請求,並把PageContext作爲一個成員變量:

public class PageCommand extends ViewCommand {

    
protected
 PageContext context;

}

PageCommand是一個DTO(data transfer object),用來傳遞請求參數和頁面結果,頁面的數據和row-id被放在PageContext對象中,調用者就不需要知道PageContext的屬性了,直接通過頁面請求讀取就可以了。 

第五步.創建一個PagingService
創建一個專門的分頁service(更多COR services 和configuration相關的看
The COR Pattern Puts Your J2EE Development on the Fast Track )處理PageCommond請求,並傳遞給PagingDAO:

public class PageService implements Service {

 
public Command process(Command command) throws ServiceException 
{
  
if (!command.getClass().equals(PageCommand.class)) 
{
   
return null
;
  }


  PageCommand pcmd 
= (PageCommand) command;

  PageContext context 
=
 pcmd.getContext();
  PageDefn desc 
=
 (PageDefn) pcmd.getDefn();

  
// Get A DAO    

  PagingDAO dao = PagingDAO.get();
  
// Issue Query, results are set into the context by the DAO

  try {

   dao.executeQuery(desc, context, 
true
);
   
return
 pcmd;
  }
 catch (DAOException e) {
   
throw new
 ServiceException(e);
  }
 }
 }

第六步.實現你的PageCommand
爲了取得第一頁,創建一個PageCommand實例,裝載你的目標view,設置所有用戶提供的參數和頁面action,傳遞給CORManager。CORManager將相應的信息交給PageService 處理:

// load view and set user supplied criteria
PageDefn d = PageDefnFactory.getPDF().createPageDefn(bundle,view);
d.setParams(
new Object[] { criteria }
);
PageCommand cmd 
= new
 PageCommand(d);

// fetch the first page

cmd.setAction(PageDefn.PageAction.FIRST);
cmd 
= (PageCommand) CORManager.get
().process(cmd);

// process results

PageContext context = cmd.getContext();

// Process results

Object[] rows = cmd.getContext().getPage();
if (rows == nullreturn
;
for (int i = 0; i < rows.length; ++i) 
{
 System.
out.println("ROW(" + i + "" +
 rows[i].toString());
}


// cache context to be reused for subsequent pages..
getServletContext().setAttribute("context",cmd.getContext());

在得到下一頁面前,確保在先前的請求中複用PageContext ,它包含所有分頁所需的信息。如果你用servlet管理分頁,在HttpSession中緩存PageCommand和PageContext對象,這樣你就可以在以後的頁面中重複使用:

// Create PageDefintion
..
PageDefn d 
=
 PageDefnFactory.getPDF().createPageDefn(bundle, view);

// Retrieve context from ServletContext

PageContext c = (PageContext) getServletContext().getAttribute("context");

// Create Page Command  

PageCommand cmd = new PageCommand(d,c);
cmd.setAction(PageDefn.PageAction.NEXT);
cmd 
= (PageCommand) CORManager.get
().process(cmd);

// cache result on servlet context

getServletContext().setAttribute("context",cmd.getContext());

當前頁數和總共頁數
你也許想知道當前頁面的頁碼和總共的頁數。你可以把這些信息顯示給用戶看,防止超過頁碼的範圍。對於基於查詢的分頁,最簡單的方法是SQL的COUNT(*)或COUNT(column name)。 你能夠通過劃分每頁的大小來確定總共的頁數。當然你不需要每次查詢的時候都執行COUNT(),在取得第一頁的數據時執行一次就可以了,這也許會導致首頁的顯示變慢。當如果數據變化的很大時,這個步驟是不需要的。
爲了增加頁碼和總共的頁數,需要擴展PageContext對象去放置數據,也要擴展PagingDAO,在首頁的請求時,執行一次count()的查詢 。

結語
絕大多數Web站點需要查詢所有的數據。分頁是約束服務器資源、大量數據查詢、數據在頁面的顯示過多的一種方法。根據需求和數據量選擇基於緩存的、基於查詢的或混合型分頁機制,可以有效的提高性能,防止內存的過量銷燬以及數據庫連接長時間的佔用。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章