开发MIS系统一个经常遇到的问题就是分页。这是一个经常出现而且没有太大变化的功能需求,我们完全可以编写一个可高度复用的解决方案。下面给出的就是一种基于Hibernate分页查询实现的一整套分页方案。在阐述这个方案之前,我想有必要就分页的相关问题做一个简单的阐述。
一般来说分页有两种解决方法:一种是一次性查出所有符合条件的数据,只是在显示的时候选出一部分展示的页面上。这种做法的好处是实现起来简单,也不会频繁地访问的数据库,但是当查询出的数据量非常大而用户可能针对某一小部分数据感兴趣时,这种做法就变得十分低效了。另一种做法则是每次只查询出一页要显示的数据,每换一页,再重新查询一次。这种做法的好处是不会一次性读出所有数据,查询速度和查询效率都很高,但不足之处是需要频繁地访问数据库(事实上,由于连接池技术的存在这种频繁的访问也未必会真得会为数据库带来多大的负担)。应该说这两种方法各有优劣,要根据实际情况进行取舍。我个人更倾向于第二种解决方法,本文提供的解决方案也是基于这种方法实现的。
一、DAO层的分页组件
DAO层的分页组件主要实现最低层的直接与数据库进行交互的分布查询功能。幸好Hibernate的API直接提供了对分页查询的支持:它们就是Query的setFirstResult方法(用于设置跳过的纪录数)和setMaxResults(用于设置一次要查出的纪录数)。一般来说分页是一个公共的需求,所以我们把分页方法写到BaseDAO里,代码如下:
public
class
BaseDAO
{
/**
* description: 根据查询字符串进行分页查询,分页的位置由skipedRows和showedRows确定
*
*
@param
queryString
* 查询字符串
*
@param
skipedRows
* 路过的纪录数
*
@param
showedRows
* 要显示的纪录数
*
@return
分布查询的结果
*/
public
List PagingQueryByHql(String queryString,
int
skipedRows,
int
showedRows)
{
List result
=
null
;
Session session
=
HibernateSessionFactory.getCurrentSession();
Query queryObject
=
session.createQuery(queryString).setFirstResult(
skipedRows).setMaxResults(showedRows);
result
=
queryObject.list();
session.close();
return
result;
}
/**
* description:根据查询字符串查询纪录总数。关于这里使用的实现方法有待进一步斟酌
*
*
@param
queryString
* 查询字符串
*
@return
纪录总数
*/
public
int
getQueryResultTotal(String queryString)
{
StringBuffer queryTotalString
=
new
StringBuffer(
"
select count(*)
"
);
queryTotalString.append(queryString.substring(queryString
.indexOf(
"
from
"
)));
Session session
=
HibernateSessionFactory.getCurrentSession();
Integer total
=
(Integer) session.createQuery(
queryTotalString.toString()).uniqueResult();
return
total.intValue();
}
}
按照B/S开发的三层架构,我们的分页功能也被分散到DAO层、业务层和Web层这三个层次,每一层关注于特定的实现,并向上一层提供服务。下面就分这三个层次来逐步展示我们的解决方案。
二、业务层的分页组件
业务层的分页组件只是简单调用DAO组件的相关方法即可,它的代码如下,其中ParticularManager是服务层定义的某个接口,ParticularManagerImpl是它的一个实现类。
public
class
ParticularManagerImpl
implements
ParticularManager
{
private
ParticularDAO;
//
一个特定DAO继承自BaseDAO,它由spring依赖注入
/**
* description: 获取查询结果,调用ParticularDAO的PagingQueryByHql方法实现分页查询业务
*
*
@param
qo
* 查询对象,携带查询关键字
*
@param
skipedRows
* 路过的纪录数
*
@param
showedRows
* 要显示的纪录数
*
@return
分页查询的结果
*/
public
List query(QO qo,
int
skipedRows,
int
showedRows)
{
//
......
}
/**
* description:获取查询结果的记录总数,调用ParticularDAO的getQueryResultTotal方法实现分页查询业务
*
*
@param
qo
* 查询对象,携带查询关键字
*/
public
int
getRecordCount(QO qo)
{
//
......
}
}
三、Web层的分页组件
Web层的分页组件包含一个 ParticularForm,和一个ParticularAction。它们的代码分别如下:
public
class
ParticularForm
extends
ActionForm
{
private
static
final
long
serialVersionUID
=
1L
;
private
QO qo;
//
查询对象qo
private
List queryResult;
//
查询结果
private
String footer
=
""
;
//
页角字符串
private
int
targetPage
=
1
;
//
目标页
private
int
pageSize
=
20
;
//
页面大小
public
void
reset(ActionMapping mapping, HttpServletRequest request)
{
qo
=
new
QO();
queryResult
=
null
;
}
/*
* 以下为各字段的getter和setter
*/
public
QO getQo()
{
return
qo;
}
public
void
setQo(QO qo)
{
this
.qo
=
qo;
}
public
List getQueryResult()
{
return
queryResult;
}
public
void
setQueryResult(List queryResult)
{
this
.queryResult
=
queryResult;
}
public
String getFooter()
{
return
footer;
}
public
void
setFooter(String footer)
{
this
.footer
=
footer;
}
public
int
getPageSize()
{
return
pageSize;
}
public
void
setPageSize(
int
pageSize)
{
this
.pageSize
=
pageSize;
}
public
int
getTargetPage()
{
return
targetPage;
}
public
void
setTargetPage(
int
targetPage)
{
this
.targetPage
=
targetPage;
}
}
public
class
ParticularAction
extends
BaseAction
{
private
PagingUtil pagingUtil
=
new
PagingUtil();
//
分页工具类
/**
* 查询信息
*
@param
mapping
*
@param
form
*
@param
request
*
@param
response
*
@return
*/
public
ActionForward query(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
{
ParticularForm particularForm
=
(ParticularForm)form;
ParticularManager particularManager
=
(ParticularManager) ServiceLocator.getService(
"
particularManager
"
);
StxxQueryQO qo
=
stxxQueryForm.getQo();
if
(qo
==
null
)
return
null
;
//
查询字符串
StringBuffer queryString
=
new
StringBuffer(
"
from BaseStxxbPO as st where 1=1
"
);
//
从qo中取出数据,组织查询字符串....
//
......
pagingUtil.setRecordTotal(particularManager.getQueryResultTotal(queryString.toString()));
pagingUtil.setPageSize(particularForm.getPageSize());
pagingUtil.setCurrentPage(particularForm.getTargetPage());
List result
=
particularManager.pagingQueryByHql(queryString.toString(),pagingUtil.getSkippedRecordTotal(), pagingUtil.getPageSize());
particularForm.setQueryResult(result);
//
将查询结果赋给ActionForm
particularForm.setFooter(pagingUtil.createSnippet());
return
mapping.findForward(
"
QueryList
"
);
//
转向列表页面
}
}
四、分页工具类
上面的类中用到了一个PagingUtil类,这是这个分页方案的又一个核心。它的代码如下:
/**
* Description: 分页工具类,实现数据分页显示
*/
public
class
PagingUtil
{
private
int
recordTotal;
//
记录总数
private
int
pageSize;
//
每页显示记录数
private
int
pageTotal;
//
总页数
private
int
currentPage;
//
当前页数
private
boolean
hasPreviousPage;
//
是否有上一页
private
boolean
hasNextPage;
//
是否有下一页
/**
* 默认构造器,将页面大小初始化为20
*/
public
PagingUtil()
{
this
.recordTotal
=
0
;
this
.pageSize
=
20
;
this
.pageTotal
=
1
;
this
.currentPage
=
1
;
this
.hasPreviousPage
=
false
;
this
.hasNextPage
=
false
;
}
/**
* 根据记录总数和页面大小进行初始化
*
*
@param
recordTotal
* 记录总数
*
@param
pageSize
* 页面大小
*/
public
PagingUtil(
int
recordTotal,
int
pageSize)
{
this
.recordTotal
=
recordTotal;
this
.pageSize
=
pageSize;
this
.currentPage
=
1
;
this
.resetPageTotal();
this
.refresh();
}
/**
* 获取已经跳过的记录总数
*
*
@return
int 跳过的记录总数
*/
public
int
getSkippedRecordTotal()
{
return
(currentPage
-
1
)
*
pageSize;
}
/**
* 刷新页面状态
*/
public
void
refresh()
{
if
(pageTotal
<=
1
)
{
hasPreviousPage
=
false
;
hasNextPage
=
false
;
}
else
if
(currentPage
==
1
)
{
hasPreviousPage
=
false
;
hasNextPage
=
true
;
}
else
if
(currentPage
==
pageTotal)
{
hasPreviousPage
=
true
;
hasNextPage
=
false
;
}
else
{
hasPreviousPage
=
true
;
hasNextPage
=
true
;
}
}
/**
* 生成页面上实现分页导航功能的JSP页面片断,以字符串形式记录.
*
*
@return
String 其内容表示实现分页导航功能的JSP页面片断,该字符串使用<bean:write>标签写到页面上时必须放置在一个form里
*/
public
String createSnippet()
{
StringBuffer snippet
=
new
StringBuffer(
""
);
if
(hasPreviousPage)
{
snippet.append(
"
<INPUT type=submit class='btn' value=首页 name=firs onclick='this.form.targetPage.value=1'>
"
);
snippet.append(
"
<INPUT type=submit class='btn' value=上页 name=prev onclick='this.form.targetPage.value=
"
+
(currentPage
-
1
)
+
"
'>
"
);
}
else
{
snippet.append(
"
<INPUT type=submit class='btn' value=首页 name=firs disabled>
"
);
snippet.append(
"
<INPUT type=submit class='btn' value=上页 name=prev disabled>
"
);
}
if
(hasNextPage)
{
snippet.append(
"
<INPUT type=submit class='btn' value=下页 name=next onclick='this.form.targetPage.value=
"
+
(currentPage
+
1
)
+
"
'>
"
);
snippet.append(
"
<INPUT type=submit class='btn' value=末页 name=last onclick='this.form.targetPage.value=
"
+
pageTotal
+
"
'>
"
);
}
else
{
snippet.append(
"
<INPUT type=submit class='btn' value=下页 name=next disabled>
"
);
snippet.append(
"
<INPUT type=submit class='btn' value=末页 name=last disabled>
"
);
}
snippet.append(
"
共
"
+
recordTotal
+
"
条记录
"
);
snippet.append(
"
每页<SELECT size=1 name=pageSize onchange='this.form.targetPage.value=1;this.form.pageSize.value=this.value;this.form.submit();'>
"
);
if
(pageSize
==
5
)
{
snippet.append(
"
<OPTION value=5 selected>5</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=5>5</OPTION>
"
);
}
if
(pageSize
==
10
)
{
snippet.append(
"
<OPTION value=10 selected>10</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=10>10</OPTION>
"
);
}
if
(pageSize
==
20
)
{
snippet.append(
"
<OPTION value=20 selected>20</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=20>20</OPTION>
"
);
}
if
(pageSize
==
50
)
{
snippet.append(
"
<OPTION value=50 selected>50</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=50>50</OPTION>
"
);
}
if
(pageSize
==
100
)
{
snippet.append(
"
<OPTION value=100 selected>100</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=100>100</OPTION>
"
);
}
snippet.append(
"
</SELECT>
"
);
snippet.append(
"
条 分
"
+
pageTotal
+
"
页显示 转到
"
);
snippet.append(
"
<SELECT size=1 name=Pagelist onchange='this.form.targetPage.value=this.value;this.form.submit();'>
"
);
for
(
int
i
=
1
; i
<
pageTotal
+
1
; i
++
)
{
if
(i
==
currentPage)
{
snippet.append(
"
<OPTION value=
"
+
i
+
"
selected>
"
+
i
+
"
</OPTION>
"
);
}
else
{
snippet.append(
"
<OPTION value=
"
+
i
+
"
>
"
+
i
+
"
</OPTION>
"
);
}
}
snippet.append(
"
</SELECT>页
"
);
snippet.append(
"
<INPUT type=hidden value=
"
+
currentPage
+
"
name="targetPage" >
"
);
snippet.append(
"
<INPUT type=hidden value=
"
+
pageSize
+
"
name="pageSize">
"
);
return
snippet.toString();
}
/**
以下为getter和setter方法 *
*/
/**
* 获取记录总数
*
*
@return
int 总行数
*/
public
int
getRecordTotal()
{
return
recordTotal;
}
/**
* 设置记录总数,同时利用recordTotal和pageSize计算出pageTotal。
*
*
@param
recordTotal 记录总数
*/
public
void
setRecordTotal(
int
recordTotal)
{
this
.recordTotal
=
recordTotal;
resetPageTotal();
refresh();
}
/**
* 计算总页数
*
*
@return
int 总页面数
*/
public
int
getPageTotal()
{
return
pageTotal;
}
/**
* 重置页面总数
*/
protected
void
resetPageTotal()
{
if
(pageSize
>=
1
)
{
pageTotal
=
(recordTotal
+
pageSize
-
1
)
/
pageSize;
}
else
{
pageTotal
=
1
;
}
}
/**
* 获取每页显示记录数
*
*
@return
int 每页显示的记录数
*/
public
int
getPageSize()
{
return
pageSize;
}
/**
*
* 设置每页显示记录数
*
*
@param
pageSize
* 每页显示记录数
*/
public
void
setPageSize(
int
pageSize)
{
this
.pageSize
=
pageSize;
resetPageTotal();
refresh();
}
/**
* 获取当前页数
*
*
@return
int 当前的页数
*/
public
int
getCurrentPage()
{
return
currentPage;
}
/**
* 设置当前页数
*
*
@return
int 当前的页数
*/
public
void
setCurrentPage(
int
currentPage)
{
if
(currentPage
>=
1
&&
currentPage
<=
pageTotal)
{
this
.currentPage
=
currentPage;
refresh();
}
}
/**
* 获取是否有下一页的状态信息
*
*
@return
blooean 是否有下一页
*/
public
boolean
isHasNextPage()
{
return
hasNextPage;
}
/**
* 获取是否有上一页的状态信息
*
*
@return
blooean 是否有上一页
*/
public
boolean
isHasPreviousPage()
{
return
hasPreviousPage;
}
}
需要特别说明的是createSnippet方法,它的作用就是根据当前页面情况动态的组织要写入到页面上的分页页面片段。而我们在页面上的工作就会因此而变得轻松了许多,因为这样一来,我们在页面上唯一要做的就是写下这一样一个语句:<bean:write name="particularForm" property="footer"/>