IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Information Management  >

使用 DB2 UDB 和 JSP 实现分页

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Naveen Balani, 技术分析师

2003 年 9 月 01 日

本文描述了如何能够通过有状态 bean 或会话来对结果集进行缓存以及利用 DB2 UDB 提供的特性来为 DB2 结果集进行分页。

简介

很多基于 Web 的应用程序都需要将用户搜索结果的某部分分成多页来显示。例如,考虑典型的搜索引擎,就说 Google 吧,这种搜索引擎将搜索结果分成多页显示,每页显示 10 条或者更多的结果。

本文将描述如何使用由 DB2® Universal Database™ (UDB) 提供的功能来实现这种设计需求,以及如何使用有状态 bean 或会话来缓存结果。我们将考虑以下两种方法:

  • 使用 DB2 的 rownext() 功能查询数据库以获得多条记录
  • 将结果缓存在有状态会话 bean 或者 HttpSession 中

本文假设读者对 J2EE 应用编程有一定的了解,并且之前使用过 DB2 UDB。





回页首


场景

我们将通过考察一个具体的场景来考虑这两种方法,该场景针对的是一个基于产品的 Web 站点,它允许用户按照类别、价格、品牌等等来搜索该站点提供的产品。搜索结果将按照用户定义的页面大小(例如每页 10 条或者 20 条结果)进行分页显示。

下面是示例的表定义:

CREATE TABLE Product_Category(
 	category_id integer not null,
 	category_name varchar(200), not null
 	category_decription varchar2(500)
 	)
CREATE TABLE Product ( 
	product_id Integer not null,
    product_name varchar2(50) not null,
    prod_category_id integer not null,
    product_decription varchar2(100) not null,
    product_price decimal(15,2),
    product_status char(1) default 'Y',
    product_width decimal(5,2) not null,
    product_length decimal(5,2) not null,
    product_created_date  timestamp,
    product_netweight  decimal(10,3),
    constraint PK_PRODUCT_ID PRIMARY KEY
    (product_id),
    constraint product_category_fk foreign key 
	(prod_category_id)
    references Product_Category (category_id))





回页首


使用 DB2 提供的功能实现分页

对于在数据库和应用程序成为产品之前可以在其中进行一些设计工作的应用程序来说,该方法比较适合。

下面我们假设,用户希望将产品按照每页 20 条结果的方式来分页显示。我们都知道,在关系表中行与行之间并没有特定的顺序。因此,需要将 SQL 查询设计为允许按照某一特定的顺序来检索数据,这可以通过对主键使用 ORDER BY 子句来实现,在这个例子中,主键就是产品 id。

DB2 允许对结果集(result set)进行动态排序,并且可以从结果集的开始部分或者结尾部分获取任意数量的行。

下面是一个查询,用于根据类别“Books”从产品表中选择前 20 条记录。

SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESCRIPTION,
  PRODUCT_PRICE, PROD_CATEGORY_ID
  FROM PRODUCT,PRODUCT_CATEGORY 
  WHERE
     PRODUCT.PROD_CATEGORY_ID
     = PRODUCT_CATEGORY.CATEGORY_ID 
  AND
     PRODUCT_CATEGORY.CATEGORY_ID = 'Books'
  ORDER BY PRODUCT.PRODUCT_ID 
  FETCH FIRST 20 ROWS ONLY 

ORDER BY 子句将强制在内存中对整个结果集进行排序,因此,为了提高 DB2 服务器的性能,我们将不使用这个子句(尽管在只发送 10 行记录到客户机时,使用 ORDER BY 子句可能会提高网络性能)。

如果不考虑顺序,而只是想要按照用户的标准获取前 20 行,那么我们就可以避免使用 ORDER BY 子句,从而就不必在 DB2 服务器上进行排序了:

SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESCRIPTION,
  PRODUCT_PRICE, PROD_CATEGORY_ID
  FROM PRODUCT,PRODUCT_CATEGORY 
  WHERE
    PRODUCT. PROD_CATEGORY_ID
    = PRODUCT_CATEGORY.CATEGORY_ID 
  AND
    PRODUCT_CATEGORY.CATEGORY_ID = 'Books'
  FETCH FIRST 20 ROWS ONLY 

既然应用程序要求每页显示 20 条结果,那么我们可以使用由 DB2 提供的 row_next() 功能。

以下是一个示例,演示了如何从编号为 21 到 40 的行中选择结果。我们假设用户定下的选择标准是:类别为“Books”,产品描述为“Application Servers”。

SELECT * FROM (
  SELECT PRODUCT_ID, PRODUCT_NAME,
    PRODUCT_DESCRIPTION, PRODUCT_PRICE, 	
    rownumber() OVER
     (ORDER BY PRODUCT_ID)
     AS ROW_NEXT 
    FROM PRODUCT,PRODUCT_CATEGORY WHERE
      PRODUCT.PROD_CATEGORY_ID
      = PRODUCT_CATEGORY.CATEGORY_ID 
    AND
      PRODUCT_CATEGORY.CATEGORY_ID = 'Books'
    AND 
      PRODUCT.PRODUCT_DESCRIPTION LIKE 
	   'Application Servers'
  )
AS PRODUCT_TEMP WHERE 
ROW_NEXT BETWEEN 21 and 40 

在一个基于 Web 的应用程序中,参数 “21” 和 “40”可以在运行时指定。下面是修改后的查询:

SELECT * FROM (
  SELECT PRODUCT_ID, PRODUCT_NAME,
    PRODUCT_DESCRIPTION, PRODUCT_PRICE, 	
    rownumber() OVER
    (ORDER BY PRODUCT_ID) AS ROW_NEXT 
    FROM PRODUCT,PRODUCT_CATEGORY
	WHERE
      PRODUCT. PROD_CATEGORY_ID
      = PRODUCT_CATEGORY.CATEGORY_ID 
    AND
      PRODUCT_CATEGORY.CATEGORY_ID = 'Books'
    AND 
      PRODUCT. PRODUCT_DESCRIPTION LIKE 
	   'Application Servers'
  )
AS PRODUCT_TEMP WHERE 
ROW_NEXT BETWEEN ? and ? 

rownumber() 函数允许开发人员动态地将行号指定给结果集。

如果去掉 row_next 子句( ROW_NEXT BETWEEN ? and ? ),那么将返回所有匹配选择标准的行。

上面使用的 SELECT * FROM 子句可以看作一个临时表,里面存有匹配选择标准的整个结果集,然后从这个临时表中返回落在给定行范围内的结果集。

使用 rownumber() 功能时对系统会有额外的性能影响,因为数据库首先要获取所有匹配选择标准的行,然后再返回落在给定范围内的那些行。

接下来,我们来权衡一下这种方法以及使用这种方法的好处,再看看在什么情况下使用这种方法比较合适。

对这种方法的权衡

  • 对事务和性能的影响
    在这种方法中,开发人员需要以编程的方式处理事务。在本场景中,假设产品每日更新一次,比如说在午夜,那么我们就可以使用最低的事务隔离级别(isolation leve l ),即 READ_UNCOMITTED。

    与没有使用 row_next 功能的查询相比,在查询中明确地使用 row_next 功能将对数据库应用程序产生一定的性能影响。而且,这种方法要求我们每次从结果集获取下一行记录时都得访问数据库,直到获得了给定范围内的所有行为止。

  • 代码的可移植性
    几乎所有的 RDBMS 系统都支持从结果集的开始部分或结尾部分获取任意数量的行,但是所使用的 SQL 语法却有所差异。因此,如果我们要更换数据库供应商,就不得不修改相应的查询。
  • 提供无缝的后退按钮功能
    如果我们的设计需要具有浏览器后退按钮功能,并且用户希望每次都能够查看最近的来自数据库的信息,那么这种设计可以提供一个可行的解决方案。其缺点是,对于每个结果页面,都可能需要访问数据库,这样就大大地影响了性能。

    基本上,URL 查询都会有内嵌的搜索参数,这些参数用于提供给 SQL Query。例如,下面就是一个可能的 URL 查询:

    http://localhost:8050/ProductSearch?minprize=50&maxprice=100&pagecount=20&resultsPerpage=10 
     

    当用户单击浏览器上的后退按钮时,将在浏览器中显示的前一个 URL 就是:

     http://localhost:8050/ProductSearch?minprize=50&maxprice=100&pagecount=10&resultsPerpage=10 
              

    我们假设,在会话中提供了最近页面的 id。因此,可以像下面这样来跟踪浏览器后退按钮:

    if(!session.getAttribute("pageID).equalsIgnoreCase
      (request.getAttribute("pageCount")){
    //Then user has clicked the back button
    //Get possible search parameters, pagecount 
    //and resultsPerPage from URL and formulate 
    //a dynamic URL and post the information back 
    //to the servlet.The URL would be
    http://localhost:8050/ProductSearch?minprize=50&
    maxprice=100&pagecount=10&resultsPerpage=10
    return;
    //Done display the results screen page
    }
    

    通过使用这种方法,用户就可以查看数据库的最新信息。

接下来,我们来看看另一种方法,这种方法在很多应用程序中都得到了广泛的使用。





回页首


这是一种广泛采用的分页方法,在这种方法中,结果被缓存在会话或者有状态 bean 中。具体使用 HttpSession 还是有状态 bean 取决于需要获取的数据多少。如果需要获取大量的数据,那么显然应该选用有状态会话 bean。

我们将利用这种方法从数据库中获取所有的结果,并将结果保存在有状态会话 bean 中。

下面是一个查询,用于获取产品表中可用的所有书:

SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESCRIPTION,
   PRODUCT_PRICE, PROD_CATEGORY_ID
   FROM PRODUCT 
   WHERE
     PRODUCT. PROD_CATEGORY_ID
     = PRODUCT_CATEGORY.CATEGORY_ID 
   AND
     PRODUCT_CATEGORY.CATEGORY_ID = 'Books'

下面是远程接口有状态 bean 的设计:

public void setSearchResults(Category category) 
throws RemoteException
public Category getPerPagSearchResults(String page_count) 
throws RemoteException

典型的 Category 和 Product 对象类似如下:

public class Category extends java.io.Serializable {
private String category_id,
private String category_name,
//Holds an Array List of products under that category.
private ArrayList products;
}
public class Product extends java.io.Serializable {
private String product_id;
private String product_name;
private String product_desc;
private String product_price;
}

下面是完成分页所需的步骤:

  1. 基于搜索标准获得结果集。
  2. 查看整个结果集,将每个产品对象保存在产品 ArrayList 中。
  3. 将产品对象的数组列表保存在类别对象中。
  4. 调用远程方法 setSearchResults 并保存类别对象。
  5. 为了获得每页显示的结果,调用远程方法 getPerPagSearchResults(),并传递 page_count (例如 10 或 20)作为参数,以获取相应的记录。

下面是典型的 getPerPagSearchResults(String page_count) 实现:

//Check if Product List contains at least page_count products
int len = category.getProduct_list().lengtd>page_count
  ? page_count : category.getProduct_list().length
ArrayList product_tempList  = new ArrayList();
for( int i = page_count+1 ; i < len ; i++){
//Loop though the product list array and get 
//corresponding records
product_tempList.add(category.getProduct_list().get(I);
}
//return category object containing an arraylist of 
//products containg records from page_count 
//to page_count+10 or product_list.length if 
//product_list.length is less than page_count+10
return product_tempList

在该实现中,可以对产品列表进行迭代,并且将所有产品信息显示出来。每当用户单击“下一页(next)”按钮的时候,下一个 page_count 便被传递给 servlet,后者将进行如上所述的相同处理。





回页首


对这种方法的权衡

  • 对事务和性能的影响
    在这种方法中,开发人员需要处理的任务是基于搜索标准获取结果集,而维护类别对象的状态以实现缓存这一任务则由 EJB 容器来处理。这就提供了另外一个层用于管理客户端状态。

    与前一种方法相比,这种方法显然可以提供更好的性能,因为搜索之后不再需要访问数据库,结果集会被缓存起来。

    既然我们要缓存结果,那么用户就可能会看到过期的数据。由于这一点,这种方法非常适合于数据更新不太频繁的应用程序。例如,如果您是在每天的午夜更新产品列表的,那么查看数据的人在使用该应用程序的全天当中都不大可能看到过期的数据。

  • 代码的可移植性
    这种方法提供了一种供应商独立(vendor-neutral)的解决方案,因为我们的缓存逻辑可以从一种 EJB 容器转移到另一种 EJB 容器,而无需更改代码。不过,如果代码需要运行在不同的 EJB 容器中的话,我们可能需要修改特定于 EJB 供应商的部署描述符。
  • 使用这种方法提供后退按钮功能
    考虑一个示例,在这个示例中浏览器缓存被禁用,而用户又单击了浏览器后退按钮。这一动作将导致浏览器再次请求 JSP 页面。现在假设我们在不同的搜索标准页面之间进行导航。按照这种设计,只有最近的搜索结果才将被显示,因为我们将最近的搜索结果保存在有状态 bean 中。因此,为了不失去用户使用的搜索标准,我们可以将搜索标准保存在会话中,当用户单击浏览器后退按钮时,便用这个搜索标准与 URL 中提供的请求参数相比较。

    下面是摘自 JSP 页面中的示例代码,该 JSP 用于显示搜索结果(Search Results)页面:

    SearchCriteria searchObjSession = (SearchCriteria) 
    session.getAttribute("SearchCriteria")
    

    接下来,我们将从 URL 中获取搜索参数。我们假设 URL 为:

    http://localhost:8050/ProductSearch?minprize=50&maxprice=100
    &pagecount=10&resultsPerpage=10
    

    下面是示例代码:

    SerachCriteria  previousSearch = new SearchCriteria();
    If(request.getAttribute("minPrice") != null){
    previousSearch.setMinPrice(request.getAttribute("minPrice"))
    //and so on for remaining parameters
    }
    

    接下来,我们将会话中的 SearchCriteria 对象与 PreviousSearch 对象相比较:

    if(!searchObjSession.equals(previousSearch)){
             
    //User is trying to view previous search results
    //Show an error to user that he is allowed to work on latest
    //search criteria only or else 
    //depending on your business logic you can give a call to
    //database with previous search 
    //criteria and store it in our Stateful bean and display the
    //results back to the user.
    //Redirect him to Search Criteria Page or Search Results Page
    // return;
    }
    





回页首


哪种方法最适合我?

根据需求,您可以应用这两种方法的组合。最广泛使用的方法是提供了结果集缓存的那种方法。

考虑一个场景,假设我们希望只显示最近添加的 20 种产品。显然,可以使用第一种方法,因为在这种情况下对结果集排序以及只显示 20 条记录都是数据库系统要做的工作。如果数据库系统是远程的,那么这一场景非常有用,因为这样可以确保只有 20 条记录在网络层上传送。





回页首


结束语

我们已经谈到了如何使用两种可能的方法来实现分页。根据应用程序和业务需求,您可以使用这两种方法中的一种来为您的 Web 应用程序解决分页问题。



关于作者

Naveen Balani Naveen Balani 将大部分时间都用在设计和开发基于 J2EE 的产品上。过去,他曾为 IBM 撰写过各种文章,涵盖的主题有 Web 服务、DB2 XML Extender、WebSphere® Studio, MQSeries®、Java® 无线设备(Wireless Devices)和 DB2 Everyplace® for Palm、Java-Nokia 以及无线数据同步(Wireless Data Synchronization)等等。可以通过 naveenbalani@rediffmail.com 与他联系。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款