Naveen Balani
技术分析师
2003 年 9 月 本文描述了如何能够通过有状态 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;
}
|
下面是完成分页所需的步骤:
- 基于搜索标准获得结果集。
- 查看整个结果集,将每个产品对象保存在产品 ArrayList 中。
- 将产品对象的数组列表保存在类别对象中。
- 调用远程方法 setSearchResults 并保存类别对象。
- 为了获得每页显示的结果,调用远程方法 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 应用程序解决分页问题。 |