编写高性能 Java 数据访问应用程序,第 3 部分: pureQuery API 最佳实践

通过代码片段和真实场景学习 pureQuery 最佳实践

pureQuery 是一种高性能 Java 数据访问平台,用于简化数据访问的开发、优化、保护和管理。它由一些工具、APIs、一个运行时、以及一些客户机监控服务组成。本系列 前两篇文章介绍了如何使用数据库访问对象(DAO)和内置内联方法来访问数据库。本文总结了使用 pureQuery API 进行开发的一些最佳实践,并提供一些真实场景,以展示如何实现这些最佳实践。 [2010 年 9 月 23 日:本文为更新版本,包含自本文于 2008 年 8 月初次发表以来产品名称的变化和其他资源。—— 编辑注]

Vitor Rodrigues, 软件开发人员, IBM

Vitor RodriguesVitor Rodrigues 是 IBM Data Studio Developer 小组的一名软件开发人员,在硅谷实验室工作。他毕业于葡萄牙米尼奥大学计算机科学与系统工程专业。Vitor 2005 年加入 IBM,当时从事 DB2 Everyplace 和 DB2 9 pureXML 方面的实习工作。在加入 Data Studio 开发小组之前,他是 DB2 pureXML 和 IBM Data Studio 的 Technical Enablement 小组的成员,在 IBM 多伦多和硅谷实验室工作。



2010 年 12 月 27 日 (最初于 2008 年 8 月 14 日)

简介

阅读重要的 pureQuery 文章

本系列前两篇文章详细介绍了如何使用内联方法和数据访问对象(DAO)来访问数据库。本文是本系列的第三篇文章,通过本文可以洞察使用 pureQuery API 进行开发的各种最佳实践。其中大多数实践利用了 pureQuery API 的高级特性。本文尽可能用真实场景演示所描述的特性的用法。文本包括的代码片段仅用于演示,但是应该有助于您理解如何使用 API。

选择内联或 DAOs

在本系列之前的文章注,作者展示了 DAOs 和使用内联方法的用例。

两种方法都有其优点,当决定使用哪种方法时,应作以下考虑:

以下情况使用 DAOs:

  • 希望您的 SQL 和您的业务逻辑分离
  • 希望通过 Optim Development Studio 生成一个简单的数据访问层
  • 希望使用 XML 文件来定义数据访问层
  • 有预定义的查询

以下情况使用 pureQuery 内联方法:

  • 希望将 SQL 语句内联在 Java 代码中,就像常规的 JDBC 编程一样
  • 有动态生成的查询

仍然不能做出决定?如果您仍然不确定应该使用哪种方法,我建议使用 DAOs。由于其隔离数据访问和业务逻辑的能力,DAOs 可以简化重构之类的任务,因为代码放在一个单独的地方,这种方法还可以简化代码重用,可以在项目之间共享数据访问接口。

集合上的查询

除了查询关系数据库之外,还可以使用 pureQuery 以相同的查询语言 — SQL 查询内存中的 Java 集合。这使得数据库和 Java 可以无缝地集成。在分布式环境中,与数据库之间的网络传输通常是最昂贵的操作之一,所以可以使用这种替代的查询方法避免这样一些昂贵的操作。

当在集合上使用查询时,可直接在已有的结果集上执行查询,而不必重新在数据库中执行查询,然后重新将所有数据取出到应用程序中。还可以使用这个特性来执行两个或更多 Java 集合之间的连接操作。

场景:显示产品目录

假设您的 Web 应用程序需要显示一个按特定品牌过滤的产品目录。在同一个 Web 页面上,右侧有一个高亮显示的帧,其中显示同一个目录中最畅销的产品。

在一个典型的应用程序中,为了生成这个页面,至少需要两次数据库调用:一次是抓取所有产品,以便在主目录中显示,另一次是抓取被浏览最多的产品,以便在屏幕右侧高亮显示。而借助 pureQuery 的集合上的查询,则可以重用包含所选类别的所有产品的第一个结果集,从而提高应用程序的性能。pureQuery 可以在这个已有的结果集上执行 SQL 语句,只过滤出状态为畅销品的产品。

在清单 1 中的例子中,使用了 pureQuery 的内联方法来实现这样的过滤,但是,DAOs 和内联方法都可以提供集合上的查询。

要查询一个 Java 内存中集合,需要获得一个与数据库连接没有关联的数据接口的实例。没有关联的连接这一事实告诉 pureQuery 要查询内存中的数据,而不是存储在数据库中的数据。

清单 1. 查询内存中已有的结果集
public void displayProducts(String category){
	Data db = DataFactory.getData(getConnection());
	List<Product> catalog = db.queryList("SELECT PID, NAME, DETAILS, " 
		+ " PRICE, WEIGHT, CATEGORY, BRAND, SIZE, "
        + " DESCRIPTION, BESTSELLER FROM PDQ_SC.PRODUCT "  
        + " where CATEGORY = ?", Product.class, category);
	for (Product p : catalog){
		//list product on webpage
	}
	Data inMemData = DataFactory.getData();
	List<Product> bestsellers = inMemData.queryList("SELECT * FROM " 
		+ " ?1.com.pureQuery.Product as prod WHERE prod.bestseller = 'Y'", 
		Product.class, catalog);
	for (Product p : bestsellers){
		//list bestseller
	}	
}

清单 1 中的第 7 行执行 Java 集合 catalog 上的一个 SQL 查询,该集合包含一个特定品牌的所有产品。注意,用于查询这个 Java 集合的 API 与用于查询数据库的 API 是相同的。如果您阅读了本系列的 编写高性能 Java 数据访问应用程序,第 2 部分:pureQuery 内联方法风格简介,您应该熟悉 API 方法 queryList。还需注意 SQL 语句中的完全限定类名。由于使用内联方法 API 的查询只在运行时执行,所以需要指定完全限定类名,以便在输入 SQL 查询时获得 pureQuery 工具提供的内容辅助,并且让 AI 知道使用哪种对象类型。

场景:生成发货报告

考虑一家电子零售公司的仓库部门。在一份订单已被支付之后,一个请求被发送到仓库,要求根据订单的内容发货。仓库有一个管理软件,该软件接收订单 ID,并使用该信息查询 ORDER_ITEMS 表,以发现订单中包含哪些产品,并将这些产品发送给顾客。知道订单中订购的所有产品之后,该软件生成一份列表,其中包含产品名称和位置(走道和仓室),以便仓库人员取出产品并将它们添加到订单包裹中。由于产品位置信息要经常用到,所以应用程序以 locator 对象的形式将它保存在内存中。下面的代码片段展示如何连接一个订单的产品和位置信息,以生成仓库人员使用的发货报告:

清单 2. 使用 pureQuery API 连接两个内存中集合
public List<ProductInfo> generateShippingReport(String orderID){
	Data db = DataFactory.getData(getConnection());
	List<Locator> locators = LocatorUtil.getLocators();
	Iterator<Product> products = db.queryIterator("SELECT p.* from PRODUCT AS p, " 
	+ " ORDER_ITEMS AS po where p.pid = po.pid and po.poid = ?", 
	Product.class, orderID);
	Data inMemData = DataFactory.getData();
	List<ProductInfo> shippingReport = inMemData.queryList("SELECT pr.pid, "  
		+ " pr.name, lr.aisle, lr.bin FROM ?1.com.pureQuery.Product AS pr, " 
		+ " ?2.com.pureQuery.Locator AS lr where pr.pid = lr.pid",   
		ProductInfo.class, products, locators);
	return shippingReport;
}

仔细观察清单 2,可以看到,位置信息是由业务逻辑生成的,而产品信息是从数据库取出的。和 清单 1 一样,这里需要创建与数据库连接没有关联的 Data 接口的实例,以便执行内存中对象上的查询。

在执行连接语句之后,shippingReport 将包含从 locatorsproducts 集合合并而来的信息。

使用 Hook 接口的可插拔回调机制

通过 pureQuery 的 Data 接口可以将语句钩子(hook)附加到它们的连接。钩子(hook)在功能上类似于数据库触发器。它为在每个 API 调用执行之前/后执行的功能提供了一种定义方式。可以在以下几个方面利用这个特性:

  • 性能监视:可以使用钩子从几个方面来度量 API 调用的运行时,例如执行时间、网络和 I/O。
  • 验证数据:通过语句钩子可以在执行语句之前验证参数数据,从而为应用程序级的约束检查和数据验证提供机会。
  • 审计 SQL:如果需要审计 pureQuery 应用程序执行的所有 SQL 语句,那么钩子可以提供一个容易的方式。

由于钩子是被附加到数据对象上的,所以应用程序感觉不到它的存在(惟一的例外是创建数据对象的代码片段)。因此,您可以实现所有上述功能,而不必重构任何代码。

场景:实现一个性能监视器

让我们看看如何使用一个钩子为应用程序实现一个性能监视解决方案。

第一步是定义一个实现 pureQuery Hook 接口的类。清单 3 展示了完成这个任务的代码。

清单 3. SystemMonitorHook,用于监视数据库访问性能
public class SystemMonitorHook implements Hook
{
  DB2SystemMonitor systemMonitor;

  public void pre (String methodName, Data dataInstance, 
  	SqlStatementType sqlStatementType, Object... parameters)
  {
    try {
     systemMonitor = ((DB2Connection)dataInstance.getConnection()).getDB2SystemMonitor();
     systemMonitor.enable (true);
     systemMonitor.start (DB2SystemMonitor.ACCUMULATE_TIMES);
    }
    catch (SQLException e) {
      throw new RuntimeException ("Unable to start system monitor " + e.getMessage ());
    }
  }

  public void post (String methodName, Data dataInstance, Object returnValue, 
  	SqlStatementType sqlStatementType, Object... parameters)
  {
    try {
      systemMonitor.stop ();
      System.out.println("Performance of method: " + methodName  + ":");
      System.out.println ("Application Time " + 
      	systemMonitor.getApplicationTimeMillis () + "milliseconds");
      System.out.println ("Core Driver Time " + 
      	systemMonitor.getCoreDriverTimeMicros () + "microseconds");
      System.out.println ("Network Time " + 
      	systemMonitor.getNetworkIOTimeMicros () + "microseconds");
      System.out.println ("server Time " + 
      	systemMonitor.getServerTimeMicros() + "microseconds");      
    }
    catch (SQLException e) {
      throw new RuntimeException
      		("Unable to stop system monitor " + e.getMessage ());
    }
  }

}

SystemMonitorHook 类实现 pureQuery Hook 接口,该接口声明了方法 pre()post()。这两个方法分别在 pureQuery API 调用执行之前和之后执行。清单 3 使用了 IBM Data Server Driver for JDBC and SQLJ(通常称为 “JCC 驱动程序)专用 API 的一部分。要学习更多关于这个 API 的知识,请参阅 DB2 Information Center 页面中关于 IBM Data Server for JDBC 的内容。

任何 pureQuery 应用程序都可以使用这个钩子。为了触发对它的使用,只需将它附加到 Data 实例上,如清单 4 所示:

清单 4. 将一个钩子与一个连接相关联
//...
Connection con = getConnection();
SystemMonitorHook monitorHook = new SystemMonitorHook();
Data data = DataFactory.getData(CustomerData.class, con, monitorHook); 
// ...

data.queryList("select * from pdq_sc.product", Product.class);

// ...

通过将钩子附加到 Data 实例上,可以激活为每个 API 调用执行的监视机制。

清单 4 中示例 queryList 调用的输出如图 1 所示:

图 1. 系统监视器钩子的输出
系统监视器钩子的输出

该监视器打印出一些性能度量,包括应用程序时间、驱动程序时间、网络时间和服务器时间。

queryList、queryArray 与 queryIterator

pureQuery 提供了 3 个返回 Java 对象集合的 API 方法:queryArrayqueryListqueryIterator。经验法则是,应该使用与应用程序期望的集合类型最匹配的那个方法,以避免类型转换。

然而,除了不同的返回类型外,这些方法还有更多值得注意的地方。这些方法幕后的工作方式同样重要,在开发应用程序时也应加以考虑。queryArray 和 queryList 对结果集进行积极的物化,而 queryIterator 则只采取消极的物化,只在 Iterator.next() 方法被调用时抓取所需的数据。

在决定使用哪个 API 方法时,可考虑以下几点提示。

在以下情况下,使用 queryArray 或 queryList:

  • 希望能够多次遍历结果集。
  • 应用程序可以分配足够的内存来将所有数据装载到集合中。

在以下情况下,使用 queryIterator:

  • 希望对结果集进行分页。在显示新的页面时随需抓取数据。
  • 应用程序没有充足的可用内存。

利用 pureQuery 批处理

接下来的两个小节描述如何利用 pureQuery 提供的批处理功能实现同构和异构的批处理。

同构批量更新

数据库应用程序常常需要在同一个操作中将一些行插入到同一个表中。JDBC 提供了批处理功能;但是,这些功能非常繁琐(需要手动设置语句参数),用起来有点复杂。

为了通过一个易于使用的 API 进行批量更新,pureQuery 内联方法提供了 updateMany 方法。该方法可用于同构批处理,并且只接收两个参数:update SQL 语句和要在更新调用中批量处理的一个 Java 对象集合。在幕后,updateMany 实现了批量更新的 JDBC 最佳实践,大大减少了更新数据时所需的网络传输。它还确保所有更新发生在同一个事务中。

场景: 更新产品数据库

每个星期,一个后端应用程序收到合作伙伴发来的更新请求以及他们要求销售的新产品列表。应用程序需要更新在线商店应用程序所使用的数据库,以便在用户浏览产品目录时显示新产品。最容易、最快捷的方式是使用 API 方法 updateMany(),如清单 5 所示:

清单 5. 同构批量更新 API 调用
//...
Data db = DataFactory.getData(getConnection());
List<Product> prods = getNewProducts();
db.updateMany("INSERT INTO PRODUCT (PRODUCTID, NAME, DETAILS, LISTPRICE,"
		+ " WEIGHT, CATEGORY, BRAND, SIZE, DESCRIPTION)"
		+ " VALUES (:productid, :name, :details, :listprice, :weight, :category,"
		+ " :brand, :size, :description)", prods);

注意,更新数据库表中的数行只需一个 API 调用。pureQuery 会批量处理变量 prods 中包含的新产品的插入。

当使用 pureQuery DAOs编程时,同构批处理是隐式的。如果一个方法以一个 bean 集合作为参数,则 pureQuery 自动将此解释为一个同构的批处理调用。

异构批量更新

虽然 pureQuery 的 updateMany 方法为在一条 SQL 语句中通过参数对一个表进行批处理提供了优化的方式,但有时候应用程序需要比这更复杂的操作,例如更新不只一个表。

场景:更新购物订单

考虑一个场景:一个顾客购物订单被存储在两个表中:ORDER 和 ORDER_ITEMS,ORDER 中包含订购的总数,而 ORDER_ITEMS 中则包含订购的每一项商品的列表。SQL 标准不支持在一条语句中对多个表执行插入/更新/删除操作,所以应用程序需要执行两条不同的语句,对每个表执行一条语句。即使使用同构批处理来更新 ORDER_ITEMS 表,应用程序仍然需要执行两次网络调用 — 一次是更新 ORDER 表,另一次是更新 ORDER_ITEMS 表。而且,还需要自己控制事务,以确保在更新这两个表之后数据库处于一致的状态。

图 2 显示了这个场景使用的 ORDER 和 ORDER_ITEMS 表之间的一对多的关系。

图 2. ORDER 和 ORDER_ITEMS 之间的一对多的关系
ORDER 和 ORDER_ITEMS 之间的一对多的关系

pureQuery 包含对异构批量更新 的支持。通过异构批量更新,可以将有或没有参数占位符的多个 SQL 语句组合到一个单独的网络调用中。在这个场景中,可以使用异构更新在一个数据库操作中更新 ORDER 和 ORDER_ITEMS 表。现有的 JDBC 批处理只支持文字语句的批处理,而 pureQuery 异构批处理则支持参数化语句的批处理。由于支持参数化语句,您可以利用这种语句提供的高级特性,例如访问路径重用和 SQL 注入预防。

同构批处理操作是在一个 API 调用中实现的,这个 API 调用只对一个表进行操作,而异构批处理操作可以包括多个 API 调用,甚至可以跨多个数据访问对象,这些 API 调用将影响到多个表。当需要在同一个事务中混合内联和方法和/或对不同方法接口的调用时,这就变得非常有用了。

清单 6 使用 pureQuery 的异构批处理来更新多个表,执行多个对 OrderData 用户定义方法接口的调用:

清单 6. 使用 pureQuery API 进行异构批量更新
public void inserPurchaseOrder(PurchaseOrder po, String poid){
	OrderData orderData = DataFactory.getData(OrderData.class, getConnection());
	//start batch
	((Data)orderData).startBatch(HeterogeneousBatchKind.heterogeneousModify__);
	//create new order
	orderData.insertNewPurchaseOrder(po);
	//add items to order
	for (OrderItem oi : po.getItems())
	{
		orderData.addItemToPurchaseOrder(poid, oi);
	}
	// end batch
	((Data)orderData).endBatch();
}

注意,方法 startBatch()endBatch() 属于 com.ibm.pdq.runtime.Data 接口,所以在调用那些方法之前,需要将 OrderData 对象覆盖为 Data。或者,也可以让 OrderData 接口扩展 Data 接口,以避免类型覆盖。在 startBatch()endBatch() 之间,可以放入要在一个批处理事务中执行的所有注释方法接口调用。在 清单 6 中,batch 块中的多个 API 调用确保与一个购物订单相关的所有信息将在一个事务中得到更新。

但是 pureQuery 的异构批处理还不止这些!除了支持在一个 OrderData 对象中执行多个操作以外,pureQuery 批处理还支持来自注释方法(DAOs)和内联方法的聚合,这意味着可以将内联方法和 DAOs 混合在同一个批处理操作中。假设您想重用上面的例子,并加上一个更新产品库存,即减去购物订单中每项产品的产品数量的操作。而且,假设您要使用内联方法执行该操作,而使用注释方法将购物订单插入到数据库中。清单 7 显示了用于实现这些更改的代码。

清单 7. 使用不同 Data 对象的异构批量更新
public void inserPurchaseOrder(PurchaseOrder po, String poid){
	Data data = DataFactory.getData(getConnection());
	OrderData orderData 	= DataFactory.getData(OrderData.class, data);
	//start batch
	data.startBatch(HeterogeneousBatchKind.heterogeneousModify__);
	//create new order
	orderData.insertNewPurchaseOrder(po);
	//add items to order
	for (OrderItem oi : po.getItems())
	{
		orderData.addItemToPurchaseOrder(poid, oi);
	}
	//update inventory
	data.updateMany("UPDATE INVENTORY SET " +
			" QUANTITY = QUANTITY - 1 WHERE PRODUCTID = :pid", 
			po.getItems());
	// end batch
	data.endBatch();
}

如清单 7 所示,批处理中同时调用了来自 dataorderData 对象的方法。即使代码引用了两个不同的对象,但这些调用是在一个批处理操作中执行的,因为这两个对象都引用相同的底层 Data 对象(注意 DataFactory.getData() 调用的第二个参数)。

清单 8. OrderData 接口
public interface OrderData {
	  //insert a new purchaseOrder
	@Update(sql = "insert into DB2ADMIN.ORDER(orderid, customerid, numberofitems, " +
	" numberofproducts, subtotaloforder, taxamount, totalamount, timestamp) " +
	" values(:orderid, :customerid, :numberofitems, :numberofproducts, " + 
	" :subtotaloforder, :taxamount, :totalamount, :timestamp)")
	void insertNewPurchaseOrder(PurchaseOrder po);
	  
	//add product to PurchaseOrder
	@Update(sql="insert into DB2ADMIN.ORDER_ITEMS(orderid, productid, quantity, cost)"
		+ " values(?1, ?2.pid, ?2.quantity, ?2.price)")
	void addItemToPurchaseOrder(String poID, OrderItem p )
}

清单 8 显示了 OrderData 接口的方法,包括 insertNewPurchaseOrderaddItemToPurchaseOrder。注意,在 addItemToPurchaseOrder 方法上,使用了有名称的参数来指定应该将 po 对象的哪个变量用作参数值。在 addItemToPurchaseOrder 方法上,同时使用了有编号和有名称的参数来引用参数值。

通过与在异构批处理例子中使用内联方法和 DAOs 相同的方式,还可以在同一个异构批处理操作中使用多个注释方法接口(DAOs)。为此,需要使用相同的基本 Data 对象创建所有 DAOs。

使用 ResultHandlers 和 RowHandlers 定制结果集

pureQuery 提供了一些基本的对象-表映射功能,这在开发数据访问层时非常有用。Optim Development Studio 为生成映射到数据库对象的 Java bean 提供了工具,以自动化这个步骤,提高生产率。

但是,有时候应用程序需要更复杂的映射,这种映射不是仅仅使用简单的对象-表映射就能实现的。有时候,只需要将一个表的子集映射到一个 Java 对象;而有时候又需要将一个表行映射到多个对象。

而且,常常需要将结果集转换成非关系格式,例如 HTML、XML 或定制的 Java 对象。

pureQuery 允许用户实现定制的映射模式,用于满足上述需求。

场景:以 HTML 格式显示多个结果集

考虑一个应用程序,这个应用程序需要以 HTML 格式显示多个结果集。自动化这一任务的一种方式是使用 pureQuery 的结果处理程序。结果处理程序用于将一个结果集的内容转换成一个对象。在接下来的例子中,结果集处理程序处理一个结果集,并返回一个 HTML 页面,这个页面以表的形式显示结果集的内容。

下面是 HTMLHandler 类的一个片段:

清单 9. 生成 HTML 页面的结果集处理程序
public class HTMLHandler implements ResultHandler<String>
{

  private DocumentBuilderFactory documentBuilderFactory_;
  private DocumentBuilder domBuilder_;
  private Transformer transformer_;

  public HTMLHandler ()
  {
	// ... initialize variables
  }

//...

  public String handle (ResultSet resultSet)
  {
    Document document = domBuilder_.newDocument ();

    // Create root element
    Element rootElement = document.createElement ("html");
    rootElement.setAttribute ("xmlns", "http://www.w3.org/TR/REC-html40");
    document.appendChild (rootElement);
    Element headElement = document.createElement ("head");
    rootElement.appendChild (headElement);
    Element titleElement = document.createElement ("title");
    titleElement.setTextContent ("HTML Table Printer");
    rootElement.appendChild (titleElement);
    Element bodyElement = document.createElement ("body");
    rootElement.appendChild (bodyElement);
    generatedTable (resultSet, bodyElement, document);
    return transformXML (document);
  }

  private void generatedTable (ResultSet resultSet, Element bodyElement, 
  		Document document)
  {
      ResultSetMetaData resultSetMetaData = resultSet.getMetaData ();
      int columnCount = resultSetMetaData.getColumnCount ();
      Element tableElement = document.createElement ("TABLE");
      tableElement.setAttribute ("border", "1");
      bodyElement.appendChild (tableElement);
      Element headerRowElement = document.createElement ("TR");
      tableElement.appendChild (headerRowElement);

      for (int index = 0; index < columnCount; index++) {
        Element headerElement = document.createElement ("TH");
        headerElement.setTextContent (resultSetMetaData.getColumnLabel (index + 1));
        headerRowElement.appendChild (headerElement);
      }
      while (resultSet.next ()) {
        Element rowElement = document.createElement ("TR");
        tableElement.appendChild (rowElement);
        for (int index = 0; index < columnCount; index++) {
          Element columnElement = document.createElement ("TD");
          columnElement.setTextContent (resultSet.getString (index + 1));
          tableElement.appendChild (columnElement);
        }
      }
  }

// ... 
}

为简单起见,清单 9 只显示 HTMLHandler.java 文件的一部分。

为了使用这个和其他结果处理程序,pureQuery API 提供了 query() 方法。这个方法接收多个参数,包括一条 SQL 语句和一个结果处理程序,并返回一个泛型 T 的对象。这个类型是由参数化接口 RowHandler<T> 的运行时类型 T 定义的。在清单 10 的例子中,HTMLHandler 处理一个结果集,并返回一个 String 类型的对象,这个对象包含一个列出所有结果集行的 Web 页面的文本表示。

要将查询的结果转换成一个 HTML 页面,只需将一个 HTMLHandler 传递给 API 调用:

清单 10. 将结果处理程序 HTMLHandler 传递给 API 调用
public String generateProductList(){
	Data db = DataFactory.getData(getConnection());
	String htmlpage =  db.query("SELECT * from PRODUCT", new HTMLHandler());
	return htmlpage;
}

场景:处理不同结构的地址

通常,数据库中的一行可以存储来自 Java 应用程序中不同对象的信息。考虑表 ADDRESS,其中包含顾客的地址。虽然这个示例只使用一个表存储该信息,但是有些国家有不同的地址结构。这常常导致未使用的列或使用相同的列存储不同的属性。例如,美国的州和加拿大的省可以存储在同一个列 “STATE” 中;但是,在 Java bean 中,却需要有分别名为 stateprovince 的变量。

我们定义 Address 接口。应用程序从数据库中获取顾客的地址,并以地址标签上常用的格式打印它们,以便将它们贴在货箱上。惟一需要实现的方法是 printableFormat(),该方法返回要打印的地址。

清单 11. 示例 Address 接口
public interface Address {

	public String printableFormat();
	
}

由于既有美国也有加拿大的顾客,所以包含 Address 接口的两个实现:

清单 12. USAddress 和 CANAddress Java 类
public class USAddress implements Address {

	protected String customerName;
	protected String street;
	protected String city;
	protected String state;
	protected String zipcode;	

	//...
}

public class CANAddress implements Address {
	protected String customerName;
	protected String street;
	protected String city;
	protected String province;
	protected String postalCode;

	//...
}

在运行时,RowHandler 决定对于当前的结果集行返回哪种对象:

清单 13. 地址结果处理程序
public class AddressHandler implements RowHandler<Address>  {
	public Address handle(ResultSet rs, Address object) throws SQLException {
		Address addr = null;
		if (rs.getString(3).equals("United States")){
			USAddress us = new USAddress();
			us.setCustomerName(rs.getString(2));
			us.setStreet(rs.getString(4));
			us.setCity(rs.getString(5));
			us.setState(rs.getString(6));
			us.setZipcode(rs.getString(7));
			addr = us;
		} else if (rs.getString(3).equals("Canada")){
			CANAddress can = new CANAddress();
			can.setCustomerName(rs.getString(2));
			can.setStreet(rs.getString(4));
			can.setCity(rs.getString(5));
			can.setProvince(rs.getString(6));
			can.setPostalCode(rs.getString(7));
			addr = can;
		}
		return addr;
	}
}

在应用程序中,以 Address 类型的对象引用所有对象,而不必处理两种类型的地址:

清单 14. 使用 AddressHandler 处理结果集
//...
Data db = DataFactory.getData(getConnection());
List<Address> addrs = db.queryList("SELECT * FROM CUSTOMERADDRESS", 
				new AddressHandler());
// process list of Address objects…

为了简单起见,这个例子中没有显示对任何接口方法的调用。在实际的应用程序中,无论是 USAddress 还是 CANAddress 对象,Address 接口会定义一些处理地址的方法。

为数据访问对象定义最适合的粒度

当使用 DAOs 方法开发应用程序时,最重要的是定义最适合的粒度。虽然如何定义这个粒度没有什么神奇的秘诀,但是有一些指南可以帮助取得最适合需求的设计:

  • 如果数据访问层非常特定于应用程序,也就是说它包含只能用于当前开发的应用程序的数据访问代码,那么最好将所有数据访问代码聚合在同一个接口中。通过这种方法,每个应用程序都有它自己的接口,从而更加易于管理。(图 3 显示了一个架构视图。)
图 3. 特定于应用程序的架构
特定于应用程序的架构
  • 另一方面,如果开发多个应用程序所需的数据访问代码,那么应该将这些代码分别放在不同的逻辑单元中,并为每个逻辑单元创建一个 DAO 接口(如图 4 所示)。通过这种方法,应用程序可以共享数据访问接口,以重用代码,减少构建整个应用程序所需的工作量。
图 4. 逻辑单元架构
逻辑单元架构

对于有些人而言,为每个数据库表创建一个 DAO 也许看上去是非常简单的方法。但是,将数据访问分别放在逻辑单元而不是数据单元可以视为更好的方法,原因如下:

  • 应用程序很少只访问一个表,所以有些操作需要实例化并使用多个接口。例如,将订单保存在数据库中需要更新 ORDER 和 ORDER_ITEMS 表,因此使用一个同时处理 ORDER 和 ORDER_ITEMS 表的 OrderData 接口比使用两个不同的接口更适合。对于抓取数据也是一样;每当从 ORDER_ITEMS 表抓取数据时,还需要获取订单信息,因此还要访问 ORDER 表。这时应该创建逻辑单元,以便将数据库访问聚合到相关对象中。
  • 如果打算使用异构批处理,则可能需要将要批处理的 SQL 语句集中到一个接口中,以便于使用异构批处理。

使用 Paging 处理程序实现分页

分页是显示大量数据的常用方法。诸如 Web 目录、多页销售报告之类的应用程序都大量使用分页,为显示的每个新页面抓取一块新的数据。

pureQuery 提供了一个名为 IteratorPagingResultHandler 的结果处理程序。和其他 ResultHandler 一样,在调用 API 时将这个处理程序传递给 Data 对象方法。这个处理程序的构造函数允许指定几个选项,包括从结果集返回的 bean 的类,甚至包括用于处理返回的每一行的 RowHandler。在指定如何返回数据时,还可以根据两种不同的模式指定从数据库返回多少数据以及哪些数据:

  • 分块检索:检索由参数 absoluteStartingRowabsoluteEndingRow 指定的区间中的所有行。
  • 分页检索:应用程序指定 pageSize 和 pageNumber 参数。处理程序返回第 pageNumber 页的数据,其中包含 pageSize 行。

清单 15 是使用 IteratorPagingResultHandler 的一个例子:

清单 15. 使用 IteratorPagingResultHandler 的分页结果
//...
int pageNumber = this.getCurrentPage();
int pageSize = this.getPageSize();
		
Iterator<Product> prods =  db.query("SELECT * from PRODUCT", 
	new IteratorPagingResultHandler<Product>(pageNumber,pageSize,Product.class));

// display Product objects…

在以上代码示例中,如果 pageNumber 值为 2,pageSize 值为 15,则变量 prods 将包含 PRODUCT 表中第 16 行到 30 行的产品。

存储过程 CallHandler

对于有多个返回值的数据库对象,存储过程有其天然的优势。SQL 语句返回结果集,UDF 则返回值(标量或表格),而存储过程除了返回多个结果集外,还在 OUT 和 INOUT 参数中返回多个值。这个特点使得存储过程成为使用 JDBC 进行处理的一种复杂的数据库资源。为了访问存储过程调用返回的所有信息,开发人员需要预先注册所有的输出参数,并将调用的结果赋给一个 ResultSet 对象。对开发人员而言幸运的是,pureQuery 提供了一个 StoredProcedureResult 对象类型,它可以用于存储一个存储过程调用返回的所有输出信息,包括输出参数和结果集。

下面的代码示例描述如何使用 StoredProcedureResult 对象类型访问一个存储过程调用的所有输出信息:

清单 16. 使用 StoredProcedureResult 处理存储过程输出
//...
int medianSalary = 75000;

StoredProcedureResult spr = db.call ("call TWO_RESULT_SETS (?)", medianSalary);

String[] outParms = (String[])spr.getOutputParms ();

System.out.println ("Output Parameter(s) length: " + outParms.length);
System.out.println ("List of Products");

Iterator<Product> prods = spr.getIterator(Product.class);
while (prods.hasNext()) {
Product p = prods.next();
System.out.println("Name: " + p.getName());
}

spr.close ();  
// ...

通过使用 StoredProcedureResult 对象,可以不必在调用存储过程之前注册输出参数。这不仅减少了输入的代码量,而且简化了过程,因为只需处理一个对象,而不必处理存储过程返回的一组输出参数和结果集。

游标处理

使 pureQuery 成为数据持久化的一种独特方法的一个方面是,虽然它提供了一些自动化步骤和映射,但是它决不会让您失去任何控制,您仍然具有与在使用纯 JDBC 编程等更低级的方法时相同的控制级别。您总是可以选择使用自动映射和 pureQuery 提供的 API,或者选择收回控制权,实现自己的结果集处理程序或连接钩子。

如果需要控制如何从数据库中抓取数据,pureQuery 为定义用于抓取数据的数据库游标的类型、并发级别和保持能力提供了选项。可以将这些设置作为参数传递给 API 方法,或者在使用DAOs时,将这些设置定义为 pureQuery 注释。这些参数的有效值反映 JDBC API 支持的值(可以参考 JDBC ResultSet API 页面 查看受支持的值)。

清单 17 显示了用于设置游标参数的一个 pureQuery API 调用:

清单 17. 在 pureQuery API 调用中设置游标属性
//...
Data db = DataFactory.getData(getConnection());
Iterator<Product> prods = db.queryIterator(java.sql.ResultSet.TYPE_FORWARD_ONLY, 
		java.sql.ResultSet.CONCUR_READ_ONLY, 
		java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT, 
		"SELECT * FROM PRODUCT", Product.class);
//...

清单 17 从数据库中将所有产品取出到一个单向游标中,这个游标以只读模式执行,并在事务被提交时关闭。

关闭资源

虽然 pureQuery 会处理大部分的资源管理工作,以提高应用程序的总体性能,但是在关闭资源方面,有些情况需要注意。

当语句、结果集和游标不再被使用时,pureQuery 会自动关闭它们。此外,当 ResultIteratorStoredProcedureResult 类型的对象中的内容已经被用完时,pureQuery 也会关闭这些对象。如果 API 调用返回这些类型之一的对象,并且您已经用完其中的所有内容,那么 pureQuery 会自动关闭这种对象。

如果应用程序逻辑会导致结果对象中的内容没有被取完,那么应该显式地关闭那些资源。一旦应用程序的数据访问部分已经完成,还应该关闭数据对象,以便释放与之关联的连接对象。

清单 18 显示了一个关闭 pureQuery 返回的 Iterator 对象的例子:

清单 18. 关闭 ResultIterator 对象
//...
Iterator<Product> prods= db.queryIterator("SELECT * from DB2ADMIN.PRODUCT",Product.class);
		
// work with iterator variable "prods"
		
((ResultIterator<Product>)prods).close();

由于 pureQuery 返回泛型 Iterator 对象,所以需要将 prods 变量转换为 ResultIterator<T> 类型的对象,因为这是 pureQuery 实现的迭代器类型,它提供了 close() 功能。

结束语

本文描述了适用于 pureQuery 开发人员的一些最佳实践。文中列出了一些高级的 API 特性,并给出了开发时用于做出决定的经验法则。遵循这些建议,您将有望提高生产率,编写更干净的代码,因为在使用 pureQuery API 时,大部分的 JDBC 负担都会消失。

希望本文能成为帮助您开发 pureQuery 应用程序和更好地利用 pureQuery 的有用的参考资料。期待您对本文和 pureQuery 的反馈。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Information Management, Java technology
ArticleID=346576
ArticleTitle=编写高性能 Java 数据访问应用程序,第 3 部分: pureQuery API 最佳实践
publish-date=12272010