zhuan(http://www.steadyxp.com/archives/187.html)
前面一篇直接使用了Myfaces中的兩個Component完成了一個簡單的分頁,這里將會介紹一種On-demand loading的方法來進(jìn)行分頁,僅僅在需要數(shù)據(jù)的時候加載。
先來說一些題外話,為了實現(xiàn)這種方式的分頁,公司里大約5-6個人做了半個多月的工作,擴展了dataTable,修改了dataScrollor,以及
各種其他的方法,但是都不是很優(yōu)雅。在上個月底的時候,在Myfaces的Mail
List中也針對這個問題展開了一系列的討論,最后有人總結(jié)了討論中提出的比較好的方法,提出了以下的分頁方法,也是目前實現(xiàn)的最為優(yōu)雅的方法,也就是不
對dataTable和dataScrollor做任何修改,僅僅通過擴展DataModel來實現(xiàn)分頁。
DataModel
是一個抽象類,用于封裝各種類型的數(shù)據(jù)源和數(shù)據(jù)對象的訪問,JSF中dataTable中綁定的數(shù)據(jù)實際上被包裝成了一個DataModel,以消除各種
不同數(shù)據(jù)源和數(shù)據(jù)類型的復(fù)雜性,在前面一篇中我們訪問數(shù)據(jù)庫并拿到了一個List,交給dataTable,這時候,JSF會將這個List包裝成
ListDataModel ,dataTable訪問數(shù)據(jù)都是通過這個DataModel進(jìn)行的,而不是直接使用List。
接下來我們要將需要的頁的數(shù)據(jù)封裝到一個DataPage中去,這個類表示了我們需要的一頁的數(shù)據(jù),里面包含有三個元
素:datasetSize,startRow,和一個用于表示具體數(shù)據(jù)的List。datasetSize表示了這個記錄集的總條數(shù),查詢數(shù)據(jù)的時候,
使用同樣的條件取count即可,startRow表示該頁的起始行在數(shù)據(jù)庫中所有記錄集中的位置。
/**
* A simple class that represents a “page” of data out of a longer set, ie a
* list of objects together with info to indicate the starting row and the full
* size of the dataset. EJBs can return instances of this type when returning
* subsets of available data.
*/
public class DataPage
{
private int datasetSize;
private int startRow;
private List data;
/**
* Create an object representing a sublist of a dataset.
*
* @param datasetSize
* is the total number of matching rows available.
*
* @param startRow
* is the index within the complete dataset of the first element
* in the data list.
*
* @param data
* is a list of consecutive objects from the dataset.
*/
public DataPage( int datasetSize, int startRow, List data)
{
this .datasetSize = datasetSize;
this .startRow = startRow;
this .data = data;
}
/**
* Return the number of items in the full dataset.
*/
public int getDatasetSize()
{
return datasetSize;
}
/**
* Return the offset within the full dataset of the first element in the
* list held by this object.
*/
public int getStartRow()
{
return startRow;
}
/**
* Return the list of objects held by this object, which is a continuous
* subset of the full dataset.
*/
public List getData()
{
return data;
}
}
接下來,我們要對DataModel進(jìn)行封裝,達(dá)到我們分頁的要求。該DataModel僅僅持有了一頁的數(shù)據(jù)DataPage,并在適當(dāng)?shù)臅r候加載數(shù)據(jù),讀取我們需要頁的數(shù)據(jù)。
/**
* A special type of JSF DataModel to allow a datatable and datascroller to page
* through a large set of data without having to hold the entire set of data in
* memory at once.
* <p>
* Any time a managed bean wants to avoid holding an entire dataset, the managed
* bean should declare an inner class which extends this class and implements
* the fetchData method. This method is called as needed when the table requires
* data that isn’t available in the current data page held by this object.
* <p>
* This does require the managed bean (and in general the business method that
* the managed bean uses) to provide the data wrapped in a DataPage object that
* provides info on the full size of the dataset.
*/
public abstract class PagedListDataModel extends DataModel
{
int pageSize;
int rowIndex;
DataPage page;
/**
* Create a datamodel that pages through the data showing the specified
* number of rows on each page.
*/
public PagedListDataModel( int pageSize)
{
super ();
this .pageSize = pageSize;
this .rowIndex = - 1 ;
this .page = null ;
}
/**
* Not used in this class; data is fetched via a callback to the fetchData
* method rather than by explicitly assigning a list.
*/
public void setWrappedData(Object o)
{
if (o instanceof DataPage)
{
this .page = (DataPage) o;
}
else
{
throw new UnsupportedOperationException( ” setWrappedData ” );
}
}
public int getRowIndex()
{
return rowIndex;
}
/**
* Specify what the “current row” within the dataset is. Note that the
* UIData component will repeatedly call this method followed by getRowData
* to obtain the objects to render in the table.
*/
public void setRowIndex( int index)
{
rowIndex = index;
}
/**
* Return the total number of rows of data available (not just the number of
* rows in the current page!).
*/
public int getRowCount()
{
return getPage().getDatasetSize();
}
/**
* Return a DataPage object; if one is not currently available then fetch
* one. Note that this doesn’t ensure that the datapage returned includes
* the current rowIndex row; see getRowData.
*/
private DataPage getPage()
{
if (page != null )
{
return page;
}
int rowIndex = getRowIndex();
int startRow = rowIndex;
if (rowIndex == - 1 )
{
// even when no row is selected, we still need a page
// object so that we know the amount of data available.
startRow = 0 ;
}
// invoke method on enclosing class
page = fetchPage(startRow, pageSize);
return page;
}
/**
* Return the object corresponding to the current rowIndex. If the DataPage
* object currently cached doesn’t include that index then fetchPage is
* called to retrieve the appropriate page.
*/
public Object getRowData()
{
if (rowIndex < 0 )
{
throw new IllegalArgumentException(
” Invalid rowIndex for PagedListDataModel; not within page ” );
}
// ensure page exists; if rowIndex is beyond dataset size, then
// we should still get back a DataPage object with the dataset size
// in it
if (page == null )
{
page = fetchPage(rowIndex, pageSize);
}
int datasetSize = page.getDatasetSize();
int startRow = page.getStartRow();
int nRows = page.getData().size();
int endRow = startRow + nRows;
if (rowIndex >= datasetSize)
{
throw new IllegalArgumentException( ” Invalid rowIndex ” );
}
if (rowIndex < startRow)
{
page = fetchPage(rowIndex, pageSize);
startRow = page.getStartRow();
}
else if (rowIndex >= endRow)
{
page = fetchPage(rowIndex, pageSize);
startRow = page.getStartRow();
}
return page.getData().get(rowIndex - startRow);
}
public Object getWrappedData()
{
return page.getData();
}
/**
* Return true if the rowIndex value is currently set to a value that
* matches some element in the dataset. Note that it may match a row that is
* not in the currently cached DataPage; if so then when getRowData is
* called the required DataPage will be fetched by calling fetchData.
*/
public boolean isRowAvailable()
{
DataPage page = getPage();
if (page == null )
{
return false ;
}
int rowIndex = getRowIndex();
if (rowIndex < 0 )
{
return false ;
}
else if (rowIndex >= page.getDatasetSize())
{
return false ;
}
else
{
return true ;
}
}
/**
* Method which must be implemented in cooperation with the managed bean
* class to fetch data on demand.
*/
public abstract DataPage fetchPage( int startRow, int pageSize);
}
最后,我們需要在Backing Bean中加一些東西,調(diào)用業(yè)務(wù)邏輯,并將數(shù)據(jù)交給PagedListDataModel,來幫我們完成最后的分頁工作。
public SomeManagedBean {
.
private DataPage getDataPage( int startRow, int pageSize) {
// access database here, or call EJB to do so
}
public DataModel getDataModel() {
if (dataModel == null ) {
dataModel = new LocalDataModel(20);
}
return dataModel;
}
private class LocalDataModel extends PagedListDataModel {
public LocalDataModel( int pageSize) {
super (pageSize);
}
public DataPage fetchPage( int startRow, int pageSize) {
// call enclosing managed bean method to fetch the data
return getDataPage(startRow, pageSize);
}
}
這里面有一個getDataPage的方法,只需要把所有業(yè)務(wù)邏輯的調(diào)用放在這里就可以了,最后業(yè)務(wù)邏輯調(diào)用的結(jié)果返回一個List,總條數(shù)返回一個int型的count放到DataPage中去就可以了。
為了實現(xiàn)復(fù)用,把上面第三段的代碼中的LocalDataModel類和getDataPage方法抽到BasePagedBackingBean中,把getDataPage方法改成:
protected abstract DataPage getDataPage(int startRow, int pageSize);
這樣我們把所有需要分頁的Backing Bean繼承自這個抽象類,并實現(xiàn)getDataPage方法即可很容易的實現(xiàn)分頁。
在具體應(yīng)用中可以這么寫:
protected DataPage getDataPage( int startRow, int pageSize)
{
List scheduleList = scheduleService.getSchedulesByDate(scheduleDate, startRow, pageSize);
int dataSetSize = scheduleService.getSchedulesCountByDate(scheduleDate);
return new DataPage(dataSetSize, startRow, scheduleList);
}
在數(shù)據(jù)訪問中,我們只需要取出我們需要行數(shù)的記錄就可以了,這在hibernate中非常容易實現(xiàn)。
如果使用Criteria查詢的話,只要加上:
criteria.setFirstResult(startRow);
criteria.setMaxResults(pageSize);
使用Query查詢的話,只要加上
query.setFirstResult(startRow);
query.setMaxResults(pageSize);
并把兩個參數(shù)傳入即可。
我們還需要另外寫一個Count的DAO,取出相同查詢條件的記錄條數(shù)即可。
還要修改一下Backing Bean中與dataTable綁定的property,將返回類型由List改成DataModel,而第一篇中用到的頁面不需要做任何修改就可以滿足新的需求了。
里面最重要的是 PagedListDataModel 中 fetchPage
這個方法,當(dāng)滿足取數(shù)據(jù)的條件時,都會調(diào)用它取數(shù)據(jù),因為業(yè)務(wù)邏輯不同,不便于將業(yè)務(wù)邏輯的調(diào)用放在里面實現(xiàn),于是將其作為抽象方法,將具體的實現(xiàn)放到具
體的Backing Bean中進(jìn)行,在BaseBackingBean中,實現(xiàn)了這個方法,調(diào)用了getDataPage(startRow,
pageSize)這個方法,而在BaseBackingBean中,這個方法又推遲到更具體的頁面中實現(xiàn),這樣,我們在具體的頁面中只需要實現(xiàn)一個
getDataPage(startRow, pageSize)這個方法訪問業(yè)務(wù)邏輯。
大功告成,這個實現(xiàn)把前面遇到的兩個問題都解決了, On-demand loading
是沒有問題了,因為只有在首次讀取和換頁的時候DataModel才會向數(shù)據(jù)庫請求數(shù)據(jù),雖然在JSF的生命周期中多次調(diào)用與dataTable綁定的方
法,但是因為每次業(yè)務(wù)邏輯請求以后,數(shù)據(jù)都會存放在DataPage中,如果里面的數(shù)據(jù)滿足需求的話,就不再請求訪問數(shù)據(jù)庫,這樣多次訪問數(shù)據(jù)庫的問題也
解決了。
雖然這樣的話,dataScrollor的Tag使用起來還是很復(fù)雜,通常在同一個項目中,我們只會使用一種樣式的分頁導(dǎo)航,不過沒關(guān)系,我們只需
要修改以下DataScrollor的Render Kit,把一些可以定義的值固定下來,再定義一個TLD文件,就可以在項目中使用簡化版的Tag了。
這個方法一開始發(fā)布在Myfaces的Wiki中,http://wiki.apache.org/myfaces/WorkingWithLargeTables,那里很少有人關(guān)注到,大家有興趣可以看看原文,本文只是對這種方法做一些簡單的介紹,并非自創(chuàng),希望大家能夠多多關(guān)注開源社區(qū),因為那里有最新最好的東西。
從Nightly Build服務(wù)器中拿到的12.27的Myfaces包,發(fā)現(xiàn)里面擴充了很多新的Component,只是并沒有正式發(fā)布,大家有興趣的話可以研究研究。