点击查询按钮,会查询出N万条数据。
后台java方法会把查询出的这N万条数据,以JSON格式传送给前台。
代码如:
view plain
JSONObject object = new JSONObject();
object.put("success", true);
object.put("root", dataList);
object.put("totalProperty", total);
//推送给前台
writeResponse(object.toString());
=======================================
/**
* ajax方式返回字符串
* @param str
* @return
*/
public boolean writeResponse(String str){
boolean ret = true;
try{
HttpServletResponse response = ServletActionContext.getResponse();
response.setContentType("text/html;charset=utf-8");
PrintWriter pw = response.getWriter();
pw.print(str);
pw.close();
}catch (Exception e) {
ret = false;
e.printStackTrace();
}
return ret;
}
=======================================
项目框架:SSH+ExtJs。
客户情况:客户等不起grid显示的loading...
开发人员:不是我的错,是数据太多,慢慢等吧
DBA意见: 不是我的错,Oracle根本不拥塞
好吧,我们来看看实现过程,一定有优化的地方,只是我们还没发现或不愿意去发现而已。
如果一次性查询出的dataList是超过千行甚至万行级的海量数据,若再将dataList以JSON格式传送给前台,势必会影响前台的显示速度。
如果给grid设置了loadMask,你一定会看到那个loading...一直在loading,直到把你loading到不耐烦。
实际上,当JSON打包的数据超过千行时,就已经有明显的等待时间了(当然,与数据库的性能也有关)。
我们知道在EXT中,如果数据量比较大时,通常给Grid配置分页工具栏PageBar,PageBar再配置grid的store,并设置pageSize,来完成数据分页显示的功能。这是最友好的显示方式。其实这也给我们暗示了一种理念,就是gird到底一次需要读取多少条数据。答案是肯定的,那就是pageSize条数据。
以此为出发点,我们会产生疑问,难道我们只需要读取pageSize条数据?那PageBar的总页数岂不是只有pageSize条数据了,这明显与实际是不符的!比如实际上如果有1W条数据,但由于pageSize设置了10条,那按照以上逻辑,只需要去读取top 10条?若数据库中只有这10条数据,那毫无疑问,PageBar的总页数就是10条了。
以上怀疑是正确的,没有错误。错误的是我们在实现上,没看清一个关键问题。那就是gird和PageBar共享同一个store。这个store有多少数据,PageBar就会有多少条数据。也就是说,gird和PageBar都傻傻地依赖着同一个store!
那要是gird和PageBar不是同一个store,且要去完成同一片数据的分页功能,那能实现吗?答案也是肯定的,这也是本文预介绍的内容:用ext实现海量数据的快速分页显示。
既然知道了gird和PageBar有了这层关系(共享同一个store,这个store总是一次性加载完所有的数据),那我们就借位思考,打破它们俩这种藕断丝连的关系:让gird和PageBar分别有自己的store!让这两个store去共享那片数据区(本来就应该这样,数据是共有的,至于谁读不读,那是他自己的事。只是EXTJS把这两个组件通过store联系在一起)。
那怎么实现呢?
原理是:GridPanel的store第一次只装载GRID_PAGE_SIZE条记录即可,PageBar的store第一次只装载总记录数。当需要翻页时,给GridPanel的store传递当前页数和PageBar显示的总条数。
第一步,让gird和PageBar分别有自己的store
view plain
var resultPanel = new top.Ext.grid.GridPanel( {
// title : '搜索结果',
ds : store,
height : 153,// autoHeight:true,
sm : new top.Ext.grid.RowSelectionModel( {
singleSelect : true
}),
cm : cm,
viewConfig : {
forceFit : true
},
// view : new top.Ext.ux.grid.BufferView({
// rowHeight : 20,
// scrollDelay : false
// }),
border : false,
columnLines : false,
frame : false,
autoScroll : true,
trackMouseOver : true,
loadMask : true,
id : 'ridGrid',
stripeRows : true,
bbar : new top.Ext.ux.DynamicPageBar( {
pageSize : GRID_PAGE_SIZE,
store : storeDetailPage,
cls : 'toolbar_bottom',
autoload : true,
displayInfo : true
})
});
可以看到grid和PageBar分别有自己的store。而它们俩的store要共享同一部分数据,就要指定同一个action URL,并且传递同样的参数,即指向同一个java方法即可。
第二步,让gird和PageBar各自的store共享同一片数据区。
其中,grid的store定义为:
view plain
var store = new top.Ext.data.Store( {
proxy : new top.Ext.data.HttpProxy( {
url : path + '/QueryAction!queryDetailData.action',
timeout : EXT_QUERY_TIME_OUT
}),
reader : reader,
listeners : {
load : function() {
isDetailReady = true;// 查询置换标志为已查过
}
}
});
PageBar的store定义为:
view plain
var storeDetailPage = new top.Ext.data.Store( {
proxy : new top.Ext.data.HttpProxy( {
url : path + '/QueryAction!queryDetailData.action',
timeout : EXT_QUERY_TIME_OUT
}),
reader : reader,
listeners : {
load : function() {
if (isDetailReady) {
if (!this.isDetailConfigured) {
// 只在第一次加载完毕的时候,为Grid重新赋数据源
resultPanel.reconfigure(storeDetailPage, cm);
storeDetailPage.baseParams.total = storeDetailPage.getTotalCount();
isDetailConfigured = true;
}
// 下一页的时候就不用重复赋数据源了
} else {
var o = this; // 解决setTimeout的参数中有this的问题
setTimeout(function() {
o.load()
}, VEHICLEPASSTORE_TIMETERVAL);// VEHICLEPASSTORE_TIMETERVAL毫秒
}
上面看到了几个变量,定义为全局变量吧:
view plain
var VEHICLEPASSTORE_TIMETERVAL = 100;// 查询时,轮训判断VehiclePassStore是否加载完毕的时间间隔,毫秒级别
var EXT_QUERY_TIME_OUT = 1200000; // EXT查询超时时间
var isDetailReady = false;// 判断Grid的store是否加载完毕
var isDetailConfigured = false;
第三步,让查询干点活--传递适当的参数
view plain
var btnSearch = new top.Ext.Button( {
width : 70,
height : 23,
text : '查询',
listeners : {
click : function() {
isDetailReady = false;
isDetailConfigured = false;
store.baseParams = {
operate : "Search",
/*参数列表区*/,
limit : 50,//grid第一次查询50条
total : 50
};
store.load();
storeDetailPage.baseParams = {
operate : "Search",
/*参数列表区*/,
limit : 50,
total : -1 //如果total是-1,即count记录数给PageBar
};
storeDetailPage.load();
}
}
});
好了,除了对应的java方法没给出外,上面的JS已经可以实现我们需要的功能了。
当然,上面是点击按钮触发的事件。如果是一打开页面就触发的话,我们就要把click : function() 里面的代码写在某个组件的
view plain
listeners : {
'afterrender' : function() {
/*here*/
}.createDelegate(this)
}
的afterrender里就可以了。
更完整的步骤:
第四步:java实现方法
Action:
view plain
public void queryDetailData() {
// 调用service,进行查询处理
Page page = QueryService.queryDetailData(start, limit,pInfo,total);
if (page != null)
{
vehicleInfo = (List) page.getResult();
if(vehicleInfo != null)
{
。。。 }
total = page.getTotalCount();
}
JSONObject object = new JSONObject();
object.put("success", true);
object.put("totalProperty", vehicleInfo);
object.put("total", page.getTotalCount());
//控制台打印
System.out.println(object.toString());
//推送给前台
writeResponse(object.toString());
// 记录日志
this.addOperationLog(...);
}
Service层:
view plain
public Page queryDetailData(int startIndex, int pageSize,String pInfo,long total){
....
return QueryDAO.queryDetailData(queryCountSql.toString(), queryResultSql.toString(),startIndex, pageSize,total);
}
DAO层:
view plain
@SuppressWarnings("unchecked")
public Page queryDetailData(final String countSql,final String resultSql,final int startIndex, final int pageSize, final long total)
{
Page result = (Page)getHibernateTemplate()
.execute(new HibernateCallback()
{
public Object doInHibernate(Session session) throws HibernateException, SQLException
{
long totalCount = 0;
if(total < 0)
{
List countlist = session.createSQLQuery(countSql).list();
for(int i=0;i<countlist.size();i++)
{
totalCount+= Long.parseLong(countlist.get(i).toString());
}
}
else
{
totalCount = total;
}
if (totalCount < 1)
{
return new Page();
}
// 实际查询返回分页对象
Query query = session.createSQLQuery(resultSql)
.addScalar("字段名1", Hibernate.STRING)
.addScalar("字段名2", Hibernate.STRING)
.addScalar("字段名3", Hibernate.STRING)
.addScalar("字段名4", Hibernate.STRING)
.addScalar("字段名5", Hibernate.STRING)
。。。
.setResultTransformer(Transformers.aliasToBean(VehicleInfo.class));
if (pageSize > 0)
{
if (startIndex < 0)
{
query.setFirstResult(0);
}
else if (startIndex >= totalCount)
{
return new Page();
}
else
{
query.setFirstResult(startIndex);
}
if(startIndex+pageSize > totalCount)
{
query.setMaxResults((int)totalCount-startIndex);
}
else
{
query.setMaxResults(pageSize);
}
}
return new Page(startIndex, totalCount, pageSize, query.list());
}
});
return result;
}