天天看点

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站点需要查询所有的数据。分页是约束服务器资源、大量数据查询、数据在页面的显示过多的一种方法。根据需求和数据量选择基于缓存的、基于查询的或混合型分页机制,可以有效的提高性能,防止内存的过量销毁以及数据库连接长时间的占用。