高级 XQuery:创建自定义函数

将软件开发的最佳实际添加到 XQuery 表达式

XQuery 函数使您只需一次性定义通用表达式,然后就可以反复重用它们。这让您获得更加紧凑、更加强壮,并且易于维护的代码。本教程通过 XQuery API for Java(XQJ)演示了如何在 Java™ 环境中实现 XQuery 函数。

Brian M. Carey, 信息系统顾问, IBM

Brian Carey 的照片Brian Carey 一位信息系统顾问,他擅长架构、设计和实现 Java 企业应用程序。



2009 年 3 月 23 日

开始之前

了解本文的内容以及如何充分利用这些知识。

关于本教程

XQuery 快速成为查询 XML 文档的行业标准。不过,与复杂的 XML 文档打过交道的人都知道,详细的 XQuery 表达式很容易变得笨拙而难以理解。另外,一些 XQuery 表达式在许多处理例程中是重复的。这就造成了表达式冗余,增加了系统维护的难度。

了解 XQuery 函数。它们提供强大的(但又容易被忽视的)XML 处理方法。这种方法有 3 个杰出的优点:可重用性强、可读性强,以及关注点分离。

本教程引导您创建 XQuery 函数,并在一个模拟的 eCommerce 环境中使用它们。

目标

本教程讨论了什么是 XQuery 函数,它们的优点是什么,以及如何实现它们。这里教您如何使用 Java Runtime Environment(JRE)和 DataDirect XQJ 包在模拟的 eCommerce 环境中实现 XQuery 函数。在教程的末尾,您将拥有自己的 XQuery 函数示例,它能够正常工作并且可以满足业务需求。

先决条件

本文针对已经理解 XML、Java 编程语言和 XQuery 的人员。要获得关于 XML、Java 编程语言和 XQuery 的更多信息,请参阅 参考资料

系统需求

要运行本教程提到的示例,您需要安装:

  • Java Standard Edition 平台(目前最新的版本是 1.6,本教程推荐使用该版本)。
  • DataDirect XQuery(XQJ 所用的库必须 在 Java 环境的类路径中。本教程中开发的 Java 类将引用它们)。

要获得这些产品的详细信息,请参阅 参考资料。开始之前,您需要下载随本教程提供的示例代码。参见 下载


XQuery 和 XQuery 函数

这个小节花点时间回顾 XQuery,并简单介绍如何使用 XQuery 函数。

XQuery 快速回顾

这很简单:XQuery 之于 XML 文档犹如 Structured Query Language(SQL)之于关系数据库。XQuery 使开发人员能够使用表达式从 XML 文档提取数据。可以提取的数据包括简单的值或文档的整个子树,比如一个元素及其所有子元素。

为了实现这个目标,XQuery 需要使用 XPath 表达式。这涉及到著名的 FLWOR 表达式:forletwhereorder byreturn。这些表达式为从 XML 文档提取和返回数据提供强大的方法。

该语言的语法基于 XML 文档本身的树状结构。XQuery 能够感知处理指令、属性和元素。

注意,要使 XQuery 能够正确地处理 XML 文档,查询不一定是有效的,但它必须 具有良好的结构。记住,具有良好结构的 XML 文档意味着它遵循 W3C 的 XML 标准(参见 参考资料)。当一个 XML 文档遵循它自身的文档类型定义(Document Type Definition,DTD)或模式时,它就是有效的。

要获得关于 XQuery 的详细解释,请参阅 参考资料

什么是 XQuery 函数

仍然使用关系数据库作为类比,XQuery 与 XML 文档的关系,就像存储过程和关系数据库的关系。那就是,它们都是其他表达式可以调用的用户自定义的例程。对于喜欢使用 Java 的人员,XQuery 函数就像实用程序类中的一个方法,它是静态声明的,但可以公开使用。

看看 清单 1 中的 XQuery 函数,我将解释它的各个部分。为了方便理解,我现在使用的函数也用于后面的模拟 eCommerce 环境中。

清单 1. XQuery 函数
declare function local:calculateReceivedIn($delay as xs:integer) 
{
	let $receivedIn := ($delay + $shippingDelay)
	return ($receivedIn)
};

这是一个非常基础的函数,但能够在 eCommerce 应用程序中很好地工作。它主要添加了两个值,一个作为该函数的参数,另一个是整个模块的独特变量。以后还会对此进行论述。

首先,要注意函数的声明。这可以通过两个词很直观地实现:declare function。这将通知 XQuery 处理程序所声明的是函数,而不是表达式。

其次,函数的名称分为两部分:local:calculateReceivedIn。冒号前面的部分(local)是名称空间,并且遵循使用名称空间的 XML 标准。在本例中,使用的是 local。一个具有行业强度的应用程序可能拥有几个模块,并且 XQuery 函数将使用特定于这些模块的名称空间。函数名称的另一部分就是:函数名。在本例中是 calculateReceivedIn,因为它计算在输出结果的 “Received in X days” 部分中显示的数字。

接下来便是参数的声明。通常,圆括号中的参数紧接着函数名。参数名以美元符号($)开始。这是标准的 XQuery 实践。注意,您还定义了参数的类型。在本例中,参数的类型为整数,因此将其定义为 as xs:integer。熟悉 XML Schema 规范的人员能够认出这种数据类型的语法。要获得完整的数据类型列表,请参见 参考资料

然后,您将开始学习函数的工作部件。它包含在大括号中,并且使用标准的 XQuery 表达式。它从计算 receivedIn 开始。这通过使用 let 表达式(let 是首字母缩写词 FLWOR 中的 L)来实现。在这里,您只是声明 receivedIn 等于 delayshippingDelay 之和。记住,shippingDelay 是一个全局性变量。在这里它不能声明为参数,因为它已经在前面的 XQuery 模块中声明。此示例会清楚地说明这点。

那么如何调用 XQuery 函数呢?看看 清单 2,考虑来自 XQuery 表达式的代码片段。

清单 2. 调用 XQuery 函数
	{local:calculateReceivedIn($minnow/availability/shipping/delay)}

这是 XML 文档的一部分。在虚构的 eCommerce 应用程序中,XML 文档被返回给应用程序,然后用于向用户显示信息。要计算这个产品(在这个例子中是一种鱼饵,形状像米诺鱼)的运输延误,需要在大括号内部调用该函数。使用完全限定的名称(包括名称和空间名),并传入从 XML 文档获取的值。这里的值来自 <delay> 元素,它必须是一个整数,否则 XQuery 会拒绝它。注意,必须使用标准的 XQueryote 表达式来获取参数的值。然后该函数从 XML 文档的 <delay> 元素接收这个值,并将其添加到 $shippingDelay 的值。它将返回这两个数值之和,如清单 2 的输出结果所示。

XQuery 函数的优点

使用 XQuery 函数有很多优点。首先,XQuery 表达式本身就是可读性很强的。看一看 清单 3

清单 3. 没有函数的 XQuery 表达式
<shipping-info>
   <received-in>
      {$minnow/availability/shipping/delay/_cnnew1@unit} 
      {
         let $delay := $minnow/availability/shipping/delay
         let $receivedIn := ($delay + $shippingDelay)
         return ($receivedIn)
      }
   </received-in>
</shipping-info>

实际上,清单 3 展示了关于运输信息的整个 XML 子树。计算 $receivedIn 的 XQuery 表达式处于中间的位置,它代替了 清单 1 中的 XQuery 函数。如上所示,没有这个函数之后,这段代码显得复杂一些,并且对于不熟悉该代码的人而言,解析它的时间也要长些。如果 XQuery 函数要执行更加复杂的工作,这段代码的可读性则会更差。

XQuery 函数的另一个优点是关注点分离功能。这个函数使开发人员能够在数据返回之前定义如何操作或处理数据,同时又可以使用 XQuery 表达式的主体部分查找函数需要处理的数据。

XQuery 表达式的最后一个优点是可重用性。在这个例子中,您可以根据一般的运输延迟和用户的地理位置定义一个函数,用于计算某一物品到达用户手中所需的时间长度。再看一看修改后的 清单 3。如果您为每个类型的物品定义了以上函数,并且添加了计算时间长度的逻辑,您则需要在整个 XQuery 表达式过程中反复执行这个操作。不过,在一个函数中定义逻辑使您能够在其他表达式中重用该逻辑。如果逻辑发生改变,您仅需在该函数内部更改它。


编写您的第一个示例

现在我们通过示例进行学习。在这一小节,您将在一个模拟的 eCommerce 环境中实现一个 XQuery 函数。

了解您的业务

如前所述,您将在一个模拟的 eCommerce 环境中实现自己的 XQuery 函数。在这个例子中,您有一个在线业务,称为 fishinhole.com。这是一个营销钓具的 Web 站点。用户浏览产品目录,订购产品,然后指定收货地址。

常用的缩写词

  • API:应用程序编程接口
  • HTML:超文本标记语言
  • IDE:集成开发环境
  • W3C:万维网联盟
  • XML:可扩展标记语言

目录中包含的产品的信息以层次结构的形式存储在一个 XML 文档中。这个 XML 文档包含每个产品的信息,比如它的名称、价格、颜色、说明,以及发货时间。Web 应用程序获取这个 XML 文档并将其转换成可以在用户的浏览器上显示的 HTML 表示。

您将实现一个用于协助特定用例的 XQuery 表达式。在这个例子中,一个经过验证的用户想要在 Web 站点上执行搜索。该用户的目的是搜索一种现状像米诺鱼的鱼饵,并且要求能在特定的时间内收到货物(例如两天以内)。您将使用 XQuery 查询该 XML 文档并只返回符合条件的鱼饵。

XQuery 函数(已在 清单 1 中展示)用来根据用户的地理位置 一般的运输延误计算用户收到鱼饵所需的时间。这是很有必要的,因为 fishinhole.com 的产品销往世界各国,并且运输时间不仅受运输延迟的影响,也受目的地的影响。

XML 文件:fishinhole.xml

看看这个 XML 文件,您将在这个用例中解析和查询它。清单 4 展示了整个 XML 文档(随本教程附带)的一小部分。尽管为了节省空间对该 XML 文档进行了删减,但清单 4 已经足以显示数据的总体结构。

清单 4. XML 文档的结构
<lures>
 <casting>
  <minnows brand="yohuri" style="deep" size="3">
   <minnow color="midnight blue">
    <international-prices>
     <price denomination="dollar">3.25</price>
     <price denomination="euro">4.25</price>
     <price denomination="canadian-dollar">4</price>
     <price denomination="peso">100</price>
    </international-prices>
    <availability>
     <shipping>
      <delay>0</delay>
     </shipping>
     <regions>
      <shipping-region>United States</shipping-region>
      <shipping-region>European Union</shipping-region>
      <shipping-region>Canada</shipping-region>
      <shipping-region>Mexico</shipping-region>
     </regions>
    </availability>
   </minnow>
  </minnows>
 </casting>
</lures>

清单 4 展示了一种鱼饵并提供了大量与之相关的信息。这些信息包括价格、运输方式、品牌名称、尺寸、样式和颜色。有些信息用属性进行描述,比如 brandstyle。其他信息使用嵌套元素进行描述,比如 <price>。注意,一些元素还对产品进行分类,比如 <casting><minnows>。这就是业务的数据结构。

尤其需要注意 <shipping> 元素。该元素的子元素是 <delay>,它描述运输延迟。前面的用例就用到这个数字。在 清单 4 的示例中,延迟为 0,它表示该产品可以在订购日达到。对于本例,运输延迟都是以天为计算单位的。

开始使用 Java 类文件

接下来,您将使用 Java 代码和 XQJ 编写 Java 类。您将编写一个简单的 Java 类,用作 XQuery 函数的单元测试。

调用这个类 XQueryFunctionTester。它将拥有一个构造函数,与 XML 文档的文件名同名。它还拥有一个 main() 方法,因此您可以执行它。清单 5 展示了这个类。

清单 5. 开始使用 Java 测试类
package com.triangleinformationsolutions.article.xquery;

import java.io.FileReader;
import java.util.Properties;

import javax.xml.namespace.QName;

import com.ddtek.xquery3.XQConnection;
import com.ddtek.xquery3.XQException;
import com.ddtek.xquery3.XQExpression;
import com.ddtek.xquery3.XQItemType;
import com.ddtek.xquery3.XQSequence;
import com.ddtek.xquery3.xqj.DDXQDataSource;

public class XQueryFunctionTester {

	private static final String XML_DOCUMENT  = "./fishinhole.xml";
	
	private String filename;

	
	public XQueryFunctionTester(String filename) {
		this.filename = filename;
	}

	
	private void go() {
	}
	
	
	public static void main(String[] args) {
		XQueryFunctionTester tester = new XQueryFunctionTester(XML_DOCUMENT);
		tester.go();
	}
}

我们先看一看 main() 方法。这是标准的 Java 应用程序的入口点。在这里,使用将要查询的 XML 文档的文件名实例化 XQueryFunctionTester。注意,我将 XML 文件置于 Java 类包结构的根部。要成功执行该类,就必须将它放到那个位置。我选择硬编码 XML 文档的引用,而不是使用命令行参数,因为使用 IDE 时,硬编码引用更加容易。如果发生了某些更改,我只需在代码上直接改动,然后重新执行。

go() 方法用于执行查询。实例化 XQueryFunctionTester 之后,就脱离了 main() 创建的静态上下文。我现在留空 go()。以后还要处理它。

此外,还要注意,我在这里包含了以后要用到的导入。这些内容大部分来自 XQJ 库,并且随后使用的 XQuery 搜索需要它们。QName 类根据 XML 规范指定一个限定名。FileReader 读取该 XML 文件。XQJ 实现需要 Properties 类。只有使用一个空的构造函数构造它,并将它作为一个方法参数传入之后才能使用它。

在这个测试用例中,用户将搜索运输延迟小于等于特定天数的所有米诺鱼诱饵。因此要编写执行该搜索的方法,如 清单 6 所示。

清单 6. 执行 XQuery 搜索的方法
private static final int SHIPPING_DELAY_IN_DAYS = 3;
private static final String DOCUMENT_NAME_FIELD = "docName";
private static final String SHIPPING_DELAY_FIELD = "shippingDelay";
private static final String MAXIMUM_DELAY_FIELD = "maximumDelay";


private XQExpression getGenericExpression() throws XQException {
	XQExpression expression = conn.createExpression();
	
	expression.bindString(new QName(DOCUMENT_NAME_FIELD), filename,
		conn.createAtomicType(XQItemType.XQBASETYPE_STRING));

	return expression;
}


private String getAllMinnowsWithMaximumDelay(int maximumDelay) throws Exception {
	XQExpression expression = getGenericExpression(); 

	expression.bindInt(new QName(SHIPPING_DELAY_FIELD), SHIPPING_DELAY_IN_DAYS,
		conn.createAtomicType(XQItemType.XQBASETYPE_INTEGER));	
	
	expression.bindInt(new QName(MAXIMUM_DELAY_FIELD), maximumDelay,
		conn.createAtomicType(XQItemType.XQBASETYPE_INTEGER));
	
	FileReader fileReader = new FileReader(GET_ALL_MINNOWS_WITH_MAX_DELAY_FILE);
	XQSequence results = expression.executeQuery(fileReader);
	
	return results.getSequenceAsString(new Properties());
}

简要地说,清单 6 中的两个方法构造了一个 XQExpression 对象,并执行它,然后以 String 格式返回结果。

getGenericExpression() 方法为扩展做好准备。如果您想在不同的测试中创建另一个方法,可以使用 getGenericExpression(),而不是为每个新的测试重新编写该方法。这个方法构造一个 XQExpression 对象并将 docName 字段设置为 XML 文件的名称。

getAllMinnowsWithMaximumDelay() 用于测试本文的用例。首先获取上面定义的泛型表达式,然后将某些变量绑定到在 Java 类中定义的值。第一个绑定的变量是 SHIPPING_DELAY_IN_DAYS,它表示虚构用户根据用户的地理位置收到货物所需的时间。在这个例子中,这个方法将变量设置为 3。下一个变量 MAXIMUM_DELAY_FIELD 是用户可以接受的最长运输时间。这个值从 maximumDelay 参数传入。最后,FileReader 读取 XQ 文件(后面将构建该文件)。这样,您就可以查询该文件并以 String 对象的形式返回结果。

注意,XQ 文件也位于类包的根部。即它和 XML 在同一个地方。

要获得更多关于 XQJ 的信息,请参阅 参考资料

清单 7 中,您将返回到 go() 方法,它执行实际的测试。

清单 7. go()init() 方法
private DDXQDataSource dataSource;
private XQConnection conn;

public void init() throws XQException {
	dataSource = new DDXQDataSource();
	conn = dataSource.getConnection();
}


private void go() {
	try {
		init();
		
		int delayToUse = 2;
		
		String returnedHtml = getAllMinnowsWithMaximumDelay(delayToUse);
		
		System.out.println(returnedHtml);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

首先,go() 方法调用 init() 方法,后者构造 XQJ 数据源并从中创建一个 XQConnection 对象。其次,要确定虚构用户能够接受的最长运输延迟。在这里仅为两天。然后调用前面描述的方法 getAllMinnowsWithMaximumDelay(),并将 delayToUse 作为参数传入。最后,就可以输出结果了。

XQuery 文件:getAllMinnowsWithMaxDelay.xq

现在,可以看一看 XQ 文件了。如 清单 8 所示。这个文件包含搜索和检索 XQuery 表达式和 XQuery 函数。刚才创建的 Java 类将引用这个文件。

清单 8. XQuery 文件
declare variable $docName as xs:string external;
declare variable $maximumDelay as xs:integer external;
declare variable $shippingDelay as xs:integer external;

declare function local:calculateReceivedIn($delay as xs:integer) 
{
	let $receivedIn := ($delay + $shippingDelay)
	return ($receivedIn)
}; 

for $minnows in doc($docName)//minnows
return
<div class="filterResults">
 <div class="brand">
	Brand: {data($minnows/@brand)}	
 </div>
 <div class="size">
	Size: {data($minnows/@size)} inches
 </div>
 {
 for $minnow in $minnows/minnow
 where $minnow/availability/shipping/delay<=$maximumDelay
 return
  <div class="singleFilterResult">
   <div class="color">
	Color: {data($minnow/@color)}
   </div>
   <div class="shippingInfo">
    <div class="receivedIn">
    Received in {local:calculateReceivedIn($minnow/availability/shipping/delay)} day(s)
    </div>
   </div>
  </div>
 }
</div>

这个 XQ 文件的总体目标是从 XML 文件提取与查询相匹配的元素,并将包含在这些元素中的信息呈现给用户。在这里,您可以在不同的类中使用一系列的 div 标记,该标记是在样式表文件中定义的。可以通过这种方式以可读美观的形式向用户呈现提取的信息。

看一看 XQ 文件的前 3 行。它们的变量名看上去很熟悉。因为您在 getAllMinnowsWithMaximumDelay()getGenericExpression() 方法中使用了这些名称。在这些方法中,值被绑定到这里定义的变量中。

接下来的几行也比较熟悉。这就是前面详细描述的函数。正是该函数返回货物运输延迟(传入的参数)和用户地理运输延迟(在文件的顶部定义的变量之一)之和。

然后是一个标准的 XQuery 表达式。这个表达式返回运输延迟天数小于等于 $maximumDelay 指定的天数的所有米诺鱼诱饵。

在代码的底部,要注意 XQuery calculateReceivedIn 函数的实现。这个函数首先使用一个标准的 XQuery 表达式提取 <delay> 元素的值,然后将这个值作为 calculateReceivedIn 函数所需的惟一参数传递。

综合

如果您已经综合这些函数,现在就可以下载本教程提供的文件了(参见 下载)。

您可以将本教程提供的压缩文件(.zip)提取到硬盘驱动器上的某个测试目录。该文件将 Java 源代码、XML 文件和 XQ 文件放到适当的位置。

接下来,编译 Java 源代码。您需要确保 ddxq.jar 在类路径中。这个库是 XQJ 发布版的一部分,它位于安装 XQJ 的目录下的 /lib 目录中。

如果编译没有问题,就可以运行这些代码了。在命令提示符处,转到刚才将代码解压缩到其中的目录,然后输入 java com.triangleinformationsolutions.article.xquery.XQueryFunctionTester 并使用适当的 -cp 参数指定指向 ddxq.jar 的类路径。只要 java.exe 在类路径中,它应该能够正常运行。

您很有可能从 IDE 运行它,而不是命令提示符。对于这种情况,只需根据 IDE 提供的指令执行一个独立的 Java 应用程序。

成功执行之后,输出结果应该类似于 清单 9。在本教程中,这个结果经过删减。

清单 9. 预期输出结果(经过删减)
<div class="filterResults">
 <div class="brand">
  Brand: Yohuri
 </div>
 <div class="size">
  Size: 3 inches
 </div>
 <div class="singleFilterResult">
  <div class="color">
   Color: midnight blue
  </div>
  <div class="shippingInfo">
   <div class="receivedIn">
    Received in 3 day(s)
   </div>
  </div>
 </div>
 <div class="singleFilterResult">
  <div class="color">
   Color: clown
  </div>
  <div class="shippingInfo">
   <div class="receivedIn">
    Received in 4 day(s)
   </div>
  </div>
 </div>
</div>

如果将 XML 文档和 清单 9 中的输出相比,您将发现它是正确的。深蓝色的鱼饵没有运输延迟(或 0 天延迟)。将它与和 $shippingDelay(该值为 3,如以上的 Java 代码所示)绑定的值相加就等于 3。杂色的鱼饵有 1 天的延迟。它和 3 相加就等于 4,所以运输延迟时间为 Received in 4 day(s)。换句话说,这个测试是成功的!


如何实现价格标签?

XQuery 函数还有其他用法吗?再看一看 清单 9 中的输出。注意,没有显示每种诱饵的价格。用户在 eCommerce 站点上购买诱饵可能是因为它提供便宜的价格。因此,要创建一个显示价格的函数。

现在您可能会问自己 “为什么需要这个函数?我可以将它从 XML 文件提取出来并显示它吗?”要记得 fishinhole.com 是国际性的,它在全球范围内做交易。因此显示的价格要符合特定国家的币种面值。如果您再次看看这个 XML,将发现它已经带有 <international-prices> 元素。

因此,在下一个用例中,要根据特定的国家确定每个产品的价格。这涉及到更改 Java 类和 XQ 文件,但不要更改 XML,因为它已经包含国际价格,如 清单 10 所示。

清单 10. 更改 Java 类
private static final String COUNTRY	= "Mexico";
private static final String COUNTRY_FIELD	= "country";
...
private String getAllMinnowsWithMaximumDelay(int maximumDelay) throws Exception {
	XQExpression expression = getGenericExpression(); 
	
	expression.bindInt(new QName(SHIPPING_DELAY_FIELD), SHIPPING_DELAY_IN_DAYS,
		conn.createAtomicType(XQItemType.XQBASETYPE_INTEGER));
	
	expression.bindInt(new QName(MAXIMUM_DELAY_FIELD), maximumDelay,
		conn.createAtomicType(XQItemType.XQBASETYPE_INTEGER));
	
	expression.bindString(new QName(COUNTRY_FIELD), COUNTRY,
		conn.createAtomicType(XQItemType.XQBASETYPE_STRING));
	
	FileReader fileReader = new FileReader(GET_ALL_MINNOWS_WITH_MAX_DELAY_FILE);
	XQSequence results = expression.executeQuery(fileReader);
	
	return results.getSequenceAsString(new Properties());
}

清单 10 与之前创建的 Java 类不同,因为它包含了两个新的静态字段和一个附加的表达式绑定。两个新的字段分别是特定国家的字段名和这个国家的实际名称。在这个示例中,测试用户居住在墨西哥,因此用户期望看到以比索显示的价格。这个新的表达式绑定将字段名(country)绑定到用户的国家名(Mexico)。

清单 11 中的新 XQ 文件与 清单 8 中的 XQ 文件没有很大的区别,但是更改还是很显著的。

清单 11. 新的 XQ 文件
declare variable $docName as xs:string external;
declare variable $maximumDelay as xs:integer external;
declare variable $shippingDelay as xs:integer external;
declare variable $country as xs:string external;

declare function local:calculateReceivedIn($delay as xs:integer) 
{
	let $receivedIn := ($delay + $shippingDelay)
	return ($receivedIn)
}; 

declare function local:getPrice($minnowElement as element(minnow)) as xs:string
{
if ($country = 'Mexico') then
(concat((data($minnowElement/international-prices/price[@denomination="peso"]))
  ," Pesos"))

else if ($country = 'Canada') then 
(concat(
 (data($minnowElement/international-prices/price[@denomination="canadian-dollar"]))
  ," Canadian Dollars"))

else if ($country = 'United States') then
(concat((data($minnowElement/international-prices/price[@denomination="dollar"]))
 ," Dollars"))

else 
(concat((data($minnowElement/international-prices/price[@denomination="euro"]))
 ," Euros"))
};

for $minnows in doc($docName)//minnows
return
<div class="filterResults">
 <div class="brand">
	Brand: {data($minnows/@brand)}	
 </div>
 <div class="size">
	Size: {data($minnows/@size)} inches
 </div>
 {
 for $minnow in $minnows/minnow
 where $minnow/availability/shipping/delay<=$maximumDelay
 return
  <div class="singleFilterResult">
   <div class="color">
	Color: {data($minnow/@color)}
   </div>
   <div class="shippingInfo">
    <div class="receivedIn">
     Received in {local:calculateReceivedIn($minnow/availability/shipping/delay)} day(s)
    </div>
    <div class="price">
     {local:getPrice($minnow)}
    </div>
   </div>
  </div>
 }
</div>

首先,您需要声明新的变量 $country。在前一个清单中就将它与值 Mexico 绑定在一起。

接下来,您将声明新的函数 getPrice。这个函数有几个需要注意的地方。首先,它采用 element 数据类型。这与前一个函数不同,后者采用简单的数据类型(integer)。这是一个很好的示例,因为它演示了不仅可以传入简单类型作为函数的参数,也可以传入元素作为参数。

这个函数需要注意的第二个地方是您指定一个返回类型。在这个例子中,返回一个字符串。因此,您不再需要在函数的主体中使用单词 return。它表明这就是返回的内容。

函数体中使用 if/then 语句,并且将 $country 的值与该应用程序支持的其他国家进行比较。如果匹配,将以所在国家的货币面值返回这个产品的价格。默认的币种是欧元。

最后,主 XQuery 表达式拥有一个新的 div 标记,它在迭代时获取当前产品的价格。此时,将调用 getPrice 函数。它将自身(即整个 <minnow> 元素)作为一个参数传入。

这个教程的代码示例分别包含针对这个示例和前一个示例的文件。这个示例的文件是 XQueryFunctionTesterIncludesPrice.java,而上一个相应的 XQ 文件是 getAllMinnowsWithMaxDelayIncludesPrice.xq。

因此,像以前一样,编译并运行 XQueryFunctionTesterIncludesPrice。执行完毕时,输出结果应该类似于 清单 12

清单 12. 新 XQ 文件的输出
<div class="filterResults">
 <div class="brand">
  Brand: Yohuri
 </div>
 <div class="size">
  Size: 3 inches
 </div>
 <div class="singleFilterResult">
  <div class="color">
   Color: midnight blue
  </div>
  <div class="shippingInfo">
   <div class="receivedIn">
	Received in 3 day(s)
   </div>
  </div>
  <div class="price">
    100 Pesos
  </div>
 </div>
</div>

这次测试又成功了!因为您将国家配置为 Mexico,所以将看到以该国的货币面值显示的价格。在这个例子中货币单位为比索。您可以清楚地看到,price 类的 div 包含 100 Pesos 作为价格。在 XML 文档中,数字 100 也是正确的。


重构和嵌套函数

解决问题的办法往往不止一个。要记住,使用 XQuery 函数的优点之一便是它的可重用性。另一个优点便是关注点分离。以此为依据,看看您创建的新函数,如 清单 13 所示。

清单 13. 新函数:尚有改善的余地?
declare function local:getPrice($minnowElement as element(minnow)) as xs:string
{
if ($country = 'Mexico') then
(concat((data($minnowElement/international-prices/price[@denomination="peso"]))
  ," Pesos"))

else if ($country = 'Canada') then 
(concat(
 (data($minnowElement/international-prices/price[@denomination="canadian-dollar"]))
  ," Canadian Dollars"))

else if ($country = 'United States') then
(concat((data($minnowElement/international-prices/price[@denomination="dollar"]))
 ," Dollars"))

else 
(concat((data($minnowElement/international-prices/price[@denomination="euro"]))
 ," Euros"))};

该函数的大部分内容都是不断重复的表达式。它在搜索方面惟一更改的地方是 denomination 属性值。这种情况为重构提供了好机会。如何对此进行重构呢?您可以已经猜到,使用另一个函数!是一个嵌套函数吗?对,没错!

有另一个函数处理搜索匹配价格的逻辑之后,如果该逻辑发生改变,您就不需要在同一个函数中对它进行 4 次更改(像 清单 13 中的函数那样)。另外,如果因扩展业务需要添加一个使用另一种货币的国家(比如印度或中国),按照原先的方法问题就更加复杂了,因为又有另一个 地方需要更改逻辑。

通过使用另一个函数,您将获得重用代码带来的两个好处,并且可以使用封装。新的函数处理从 XML 文档体获取实际价格的必要业务逻辑。

因此,创建一个称为 getCountrySpecificPrice 的新函数。这个函数接受两个参数:上述的 <minnow> 元素和表示货币面值名称的属性的实际名称。然后,函数将从该元素获取值作为一个 denomination 属性值。这个属性值与作为参数传递给该函数的名称相匹配。

清单 14 不仅展示了新函数,还演示了如何重写 getPrice 函数,使其包含新的函数。

清单 14. 改进后的新函数
declare function local:getCountrySpecificPrice($minnowElement as element(minnow),
$denomination as xs:string) {
let $price := 
   (data($minnowElement/international-prices/price[@denomination=$denomination]))
 
return ($price)
};

declare function local:getPrice($minnowElement as element(minnow)) as xs:string
{
if ($country = 'Mexico') then
(concat(local:getCountrySpecificPrice($minnowElement,"peso")," Pesos"))

else if ($country = 'Canada') then 
(concat(local:getCountrySpecificPrice($minnowElement,"canadian-dollar")
 ," Canadian Dollars"))

else if ($country = 'United States') then 
(concat(local:getCountrySpecificPrice($minnowElement,"dollar")," Dollars"))

else (concat(local:getCountrySpecificPrice($minnowElement,"euro")," Euros"))};

如您所见,最后修改的代码更加干净了。这也很重要,因为它演示了如何在函数中嵌套函数。

本教程的用例以此结束。不过,使用 XQuery 函数可以对本教程给出的 XML 文档和 XQ 文件进行其他实践。您可以开始探索自己喜欢的事情。使用这些源文件实现自己的用例和相应的测试,并且要经常进行备份。


结束语

本教程的目的是详细阐述 XQuery 函数。我解释了什么是 XQuery 函数,以及使用、定义和实现它们有什么优点。

我通过一个模拟的 eCommerce 环境演示了一些有效的 XQuery 函数实现及其用例。在这个例子中,模拟的 eCommerce 环境是一个向世界各地营销鱼饵的 Web 站点。这些函数的功能是提取该公司提供的与每种鱼饵相关的特定信息。

您通过 Java 编程语言为 XQuery 函数编程了测试。这些测试是标准的 Java 类,它们在 JRE 中执行,并且使用 DataDirect XQJ 库处理 XQuery 实现。要检查测试的结果,您仅需查看控制台的输出。

您还在 XQuery 函数内部执行了一些重构。我演示了如何将面向对象的概念应用到 XQuery 函数,比如封装。使用这种方式可以将特定的业务逻辑指定给特定的函数。如果逻辑发生改变,您只需要在一个地方更改相应的代码。

总结

总体来说,XQuery 函数为关注点分离、实现可重用性提供了强大的方法,并且为其他开发人员提供可读性很强的代码。开发人员通常忽略它们,因此编写出更加复杂、更加难维护的代码。XQuery 代码的调试和维护十分艰辛,不过只要正确地实现 XQuery,这些任务就变得十分简单。


下载

描述名字大小
本教程的源代码xqueryfunctions.zip9KB

参考资料

学习

获得产品和技术

讨论

条评论

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=XML, Java technology
ArticleID=377889
ArticleTitle=高级 XQuery:创建自定义函数
publish-date=03232009