天天看點

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語句的細節:

J2EE中的分頁
J2EE中的分頁

public class PageDefn extends ViewDefn 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public interface PageAction 

J2EE中的分頁

{

J2EE中的分頁

      public static final int FIRST = 0;

J2EE中的分頁

      public static final int NEXT = 1;

J2EE中的分頁

      public static final int PREVIOUS = 2;

J2EE中的分頁

      public static final int CURRENT = 3;

J2EE中的分頁

   }

J2EE中的分頁
J2EE中的分頁

     protected int pageSize;

J2EE中的分頁

     protected ColumnDesc[] rowIds;

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public PageDefn() 

J2EE中的分頁

{

J2EE中的分頁

      super();

J2EE中的分頁

      rowIds = null;

J2EE中的分頁

      pageSize = 50;

J2EE中的分頁

    }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public PageDefn(String viewname) 

J2EE中的分頁

{

J2EE中的分頁

      super(viewname);

J2EE中的分頁

      rowIds = null;

J2EE中的分頁

      pageSize = 50;

J2EE中的分頁

   }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

}

J2EE中的分頁

第三步.建立一個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和結果,并封裝了分頁資訊:

J2EE中的分頁
J2EE中的分頁

public class PageContext implements Serializable 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁

 protected Object page; // results

J2EE中的分頁

 protected Object firstEntry; //  first row ID 

J2EE中的分頁

 protected Object lastEntry; // last row id

J2EE中的分頁

 protected int action; // paging action

J2EE中的分頁

 protected PageContext prevContext; // previous context state

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public PageContext() 

J2EE中的分頁

{

J2EE中的分頁

  this.page = new Object[0];

J2EE中的分頁

  this.firstEntry = null;

J2EE中的分頁

  this.lastEntry = null;

J2EE中的分頁

  this.action = PageDefn.PageAction.FIRST;

J2EE中的分頁

 }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public Object[] getPage() 

J2EE中的分頁

{

J2EE中的分頁

  return ((Collection) page).toArray();

J2EE中的分頁

 }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public void setPage(Object page) 

J2EE中的分頁

{

J2EE中的分頁

  this.page = page;

J2EE中的分頁

 }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public int getAction() 

J2EE中的分頁

{

J2EE中的分頁

  return action;

J2EE中的分頁

 }

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public void setAction(int action) 

J2EE中的分頁

{

J2EE中的分頁

  this.action = action;

J2EE中的分頁

 }

J2EE中的分頁

}

J2EE中的分頁
J2EE中的分頁

確定你的PageContext對象是可序列化的,它可用于傳輸。建立PageCommand封裝你的頁面請求,并把PageContext作為一個成員變量:

J2EE中的分頁
J2EE中的分頁

public class PageCommand extends ViewCommand 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁

     protected PageContext context;

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

}

J2EE中的分頁
J2EE中的分頁

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:

J2EE中的分頁
J2EE中的分頁

public class PageService implements Service 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁
J2EE中的分頁

 public Command process(Command command) throws ServiceException 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁

  if (!command.getClass().equals(PageCommand.class)) 

J2EE中的分頁

{

J2EE中的分頁

   return null;

J2EE中的分頁

  }

J2EE中的分頁
J2EE中的分頁

  PageCommand pcmd = (PageCommand) command;

J2EE中的分頁
J2EE中的分頁

  PageContext context = pcmd.getContext();

J2EE中的分頁

  PageDefn desc = (PageDefn) pcmd.getDefn();

J2EE中的分頁
J2EE中的分頁

  // Get A DAO    

J2EE中的分頁

  PagingDAO dao = PagingDAO.get();

J2EE中的分頁

  // Issue Query, results are set into the context by the DAO

J2EE中的分頁
J2EE中的分頁

  try 

J2EE中的分頁

{

J2EE中的分頁
J2EE中的分頁

   dao.executeQuery(desc, context, true);

J2EE中的分頁

   return pcmd;

J2EE中的分頁
J2EE中的分頁

  } catch (DAOException e) 

J2EE中的分頁

{

J2EE中的分頁

   throw new ServiceException(e);

J2EE中的分頁

  } } }

J2EE中的分頁
J2EE中的分頁

第六步.實作你的PageCommand

為了取得第一頁,建立一個PageCommand執行個體,裝載你的目标view,設定所有使用者提供的參數和頁面action,傳遞給CORManager。CORManager将相應的資訊交給PageService 處理:

J2EE中的分頁

// load view and set user supplied criteria

J2EE中的分頁

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

J2EE中的分頁
J2EE中的分頁

d.setParams(new Object[] 

J2EE中的分頁

{ criteria } );

J2EE中的分頁

PageCommand cmd = new  PageCommand(d);

J2EE中的分頁
J2EE中的分頁

// fetch the first page

J2EE中的分頁

cmd.setAction(PageDefn.PageAction.FIRST);

J2EE中的分頁

cmd = (PageCommand) CORManager.get ().process(cmd);

J2EE中的分頁
J2EE中的分頁

// process results

J2EE中的分頁

PageContext context =  cmd.getContext();

J2EE中的分頁
J2EE中的分頁

// Process results

J2EE中的分頁

Object[] rows =  cmd.getContext().getPage();

J2EE中的分頁

if (rows == null) return ;

J2EE中的分頁
J2EE中的分頁

for (int i = 0; i < rows.length; ++i) 

J2EE中的分頁

{

J2EE中的分頁

 System.out.println("ROW(" + i + ") " + rows[i].toString());

J2EE中的分頁

}

J2EE中的分頁
J2EE中的分頁

// cache context to be reused for subsequent pages..

J2EE中的分頁

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

J2EE中的分頁
J2EE中的分頁

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

J2EE中的分頁

// Create PageDefintion

J2EE中的分頁

..

J2EE中的分頁

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

J2EE中的分頁
J2EE中的分頁

// Retrieve context from ServletContext

J2EE中的分頁

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

J2EE中的分頁
J2EE中的分頁

// Create Page Command  

J2EE中的分頁

PageCommand cmd = new  PageCommand(d,c);

J2EE中的分頁

cmd.setAction(PageDefn.PageAction.NEXT);

J2EE中的分頁

cmd = (PageCommand) CORManager.get ().process(cmd);

J2EE中的分頁
J2EE中的分頁

// cache result on servlet context

J2EE中的分頁

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

J2EE中的分頁
J2EE中的分頁

目前頁數和總共頁數

你也許想知道目前頁面的頁碼和總共的頁數。你可以把這些資訊顯示給使用者看,防止超過頁碼的範圍。對于基于查詢的分頁,最簡單的方法是SQL的COUNT(*)或COUNT(column name)。 你能夠通過劃分每頁的大小來确定總共的頁數。當然你不需要每次查詢的時候都執行COUNT(),在取得第一頁的資料時執行一次就可以了,這也許會導緻首頁的顯示變慢。當如果資料變化的很大時,這個步驟是不需要的。

為了增加頁碼和總共的頁數,需要擴充PageContext對象去放置資料,也要擴充PagingDAO,在首頁的請求時,執行一次count()的查詢 。

結語

絕大多數Web站點需要查詢所有的資料。分頁是限制伺服器資源、大量資料查詢、資料在頁面的顯示過多的一種方法。根據需求和資料量選擇基于緩存的、基于查詢的或混合型分頁機制,可以有效的提高性能,防止記憶體的過量銷毀以及資料庫連接配接長時間的占用。