以文本方式查看主题 - 胡子鱼JAVA FANS论坛 (http://www.3doing.net/forums/index.asp) -- 『 JSP/Servlets/JSF 』 (http://www.3doing.net/forums/list.asp?boardid=8) ---- 在JSF中实现分页 (http://www.3doing.net/forums/dispbbs.asp?boardid=8&id=1432) |
-- 作者:bibiye -- 发布时间:2006-1-29 23:13:34 -- 在JSF中实现分页 对于大多数Web应用,分页都是必不可少的功能,当然在JSF中也一样,我在这里用两篇文章介绍两种方法来展示一下,如何在JSF中实现分页。本文假定你已经对JSF有了一些简单的了解,懂得基本配置和使用,并建立起一个blank项目。
Myfaces是Apache基金会中的一个一级项目,除了实现JSF标准外,做了很多的扩展工作,在Myfaces包中有一个扩展包Tomahawk,我们将主要使用其中的两个Component实现分页:一个是<t:dataTable>,另一个是<t:dataScroller>,在第一篇里面,我们简易的组合这两个Component来实现一种简单,但并不高效的分页。
下面的例子来自于Myfaces-Sample,我省去了其中和分页逻辑无关的内容,详细的例子可以下载Myfaces-Sample包或者访问http://www.irian.at/myfaces/home.jsf 查看。
第一部分:dataTable
<t:dataTable id="data"
var="car" value="#{pagedSort.cars}" rows="10"> …… </t:dataTable> 在这一部分中,dataTable绑定了一个backing bean - pagedSort中的cars属性,我们可以在这个属性中加入数据访问逻辑,从数据库或者其他来源取得用于显示的数据。比如我们可以通过Hibernate获取一个List,其中包含有我们用于显示的POJOs。
注意,dataTable中的rows属性指的是每页的行数,是必须指定的,否则是无法进行分页的,如果在项目中会使用固定行数的分页,建议把这个值写在BaseBackingBean中,并暴露一个property,供页面调用,所以每次在页面中就可以这么写#{backingBean.pageSize}。
第二部分:dataScroller
<t:dataScroller id="scroll_1"
for="data" fastStep="10" paginator="true" paginatorMaxPages="9"> <f:facet name="first" > <t:graphicImage url="images/arrow-first.gif" border="1" /> </f:facet> <f:facet name="last"> <t:graphicImage url="images/arrow-last.gif" border="1" /> </f:facet> <f:facet name="previous"> <t:graphicImage url="images/arrow-previous.gif" border="1" /> </f:facet> <f:facet name="next"> <t:graphicImage url="images/arrow-next.gif" border="1" /> </f:facet> <f:facet name="fastforward"> <t:graphicImage url="images/arrow-ff.gif" border="1" /> </f:facet> <f:facet name="fastrewind"> <t:graphicImage url="images/arrow-fr.gif" border="1" /> </f:facet> </t:dataScroller>
这里定义了我们用于分页的<t:dataScroller>,最主要的是配置该分页Component针对哪个dataTable进行分页的“for”属性,该属性与dataTable绑定,并对其进行分页,在这里,绑定了第一部分中的id="data"的dataTable,下面有很多的<t:facet>是指定分页的导航样式的,这里使用了图片作为导航,可以把他们改成文字形式的导航。
当然这只是最简单,也是一种不推荐的分页方式,因为在每次进行分页的时候,将会从数据库中取回所有的记录放入List中,然后,dataScroller在对这个List进行分页,如果在数据量很大的情况下,这种方式显然是不符合要求的,假设每条记录占用1k内存,数据库中有100万条记录,每次要把这个List全部读取出来将占用1G内存。我们需要一种Load on demand方式的读取,也就是只在需要查看某页的时候读取该页的数据。
另外一方面,JSF的生命周期中有多个阶段会调用到#{pagedSort.cars}中对应的方法,如果在这里调用了数据访问逻辑,就会在只显示一次页面的情况下进行多次数据库操作,也是相当的耗费资源的。
所以我们需要有更好的分页方式去解决以上问题,下一篇我将介绍另一种方法以改善这些问题。
前面一篇直接使用了Myfaces中的两个Component完成了一个简单的分页,这里将会介绍一种On-demand loading的方法来进行分页,仅仅在需要数据的时候加载。
先来说一些题外话,为了实现这种方式的分页,公司里大约5-6个人做了半个多月的工作,扩展了dataTable,修改了dataScrollor,以及各种其他的方法,但是都不是很优雅。在上个月底的时候,在Myfaces的Mail List中也针对这个问题展开了一系列的讨论,最后有人总结了讨论中提出的比较好的方法,提出了以下的分页方法,也是目前实现的最为优雅的方法,也就是不对dataTable和dataScrollor做任何修改,仅仅通过扩展DataModel来实现分页。
DataModel是一个抽象类,用于封装各种类型的数据源和数据对象的访问,JSF中dataTable中绑定的数据实际上被包装成了一个DataModel,以消除各种不同数据源和数据类型的复杂性,在前面一篇中我们访问数据库并拿到了一个List,交给dataTable,这时候,JSF会将这个List包装成ListDataModel,dataTable访问数据都是通过这个DataModel进行的,而不是直接使用List。
接下来我们要将需要的页的数据封装到一个DataPage中去,这个类表示了我们需要的一页的数据,里面包含有三个元素:datasetSize,startRow,和一个用于表示具体数据的List。datasetSize表示了这个记录集的总条数,查询数据的时候,使用同样的条件取count即可,startRow表示该页的起始行在数据库中所有记录集中的位置。
/** *//**
* 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进行封装,达到我们分页的要求。该DataModel仅仅持有了一页的数据DataPage,并在适当的时候加载数据,读取我们需要页的数据。
/** *//**
* 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中加一些东西,调用业务逻辑,并将数据交给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(getRowsPerPage()); } 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的方法,只需要把所有业务逻辑的调用放在这里就可以了,最后业务逻辑调用的结果返回一个List,总条数返回一个int型的count放到DataPage中去就可以了。
为了实现复用,把上面第三段的代码中的LocalDataModel类和getDataPage方法抽到BasePagedBackingBean中,把getDataPage方法改成:
protected abstract DataPage getDataPage(int startRow, int pageSize);
这样我们把所有需要分页的Backing Bean继承自这个抽象类,并实现getDataPage方法即可很容易的实现分页。
在具体应用中可以这么写:
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); }
在数据访问中,我们只需要取出我们需要行数的记录就可以了,这在hibernate中非常容易实现。
如果使用Criteria查询的话,只要加上:
criteria.setFirstResult(startRow);
criteria.setMaxResults(pageSize);
使用Query查询的话,只要加上
query.setFirstResult(startRow);
query.setMaxResults(pageSize);
并把两个参数传入即可。
我们还需要另外写一个Count的DAO,取出相同查询条件的记录条数即可。
还要修改一下Backing Bean中与dataTable绑定的property,将返回类型由List改成DataModel,而第一篇中用到的页面不需要做任何修改就可以满足新的需求了。
里面最重要的是 PagedListDataModel 中 fetchPage 这个方法,当满足取数据的条件时,都会调用它取数据,因为业务逻辑不同,不便于将业务逻辑的调用放在里面实现,于是将其作为抽象方法,将具体的实现放到具体的Backing Bean中进行,在BaseBackingBean中,实现了这个方法,调用了getDataPage(startRow, pageSize)这个方法,而在BaseBackingBean中,这个方法又推迟到更具体的页面中实现,这样,我们在具体的页面中只需要实现一个getDataPage(startRow, pageSize)这个方法访问业务逻辑。
大功告成,这个实现把前面遇到的两个问题都解决了,On-demand loading是没有问题了,因为只有在首次读取和换页的时候DataModel才会向数据库请求数据,虽然在JSF的生命周期中多次调用与dataTable绑定的方法,但是因为每次业务逻辑请求以后,数据都会存放在DataPage中,如果里面的数据满足需求的话,就不再请求访问数据库,这样多次访问数据库的问题也解决了。
虽然这样的话,dataScrollor的Tag使用起来还是很复杂,通常在同一个项目中,我们只会使用一种样式的分页导航,不过没关系,我们只需要修改以下DataScrollor的Render Kit,把一些可以定义的值固定下来,再定义一个TLD文件,就可以在项目中使用简化版的Tag了。
这个方法一开始发布在Myfaces的Wiki中,http://wiki.apache.org/myfaces/WorkingWithLargeTables,那里很少有人关注到,大家有兴趣可以看看原文,本文只是对这种方法做一些简单的介绍,并非自创,希望大家能够多多关注开源社区,因为那里有最新最好的东西。
从Nightly Build服务器中拿到的12.27的Myfaces包,发现里面扩充了很多新的Component,只是并没有正式发布,大家有兴趣的话可以研究研究。
|
|