更新XQuery
XQuery有了一些新特性,内容包括从原子化到跟踪文件结构。
在"你所不了解的XQuery"(Oracle杂志,2003年5/6月刊)一文中,我介绍了XQuery,它是一项由万维网联盟(W3C)开发的技术,设计用来查询和操纵XML数据或任何能以XML形式出现的数据,如关系型数据库。那篇引文讨论了2002年11月发布的XQuery草案规范。2003年5月,W3C发布了新的XQuery草案规范,本文追踪报道了5月份发布的草案规范中最令人感爱好的变化和新增加的特性,其中包括库模块、序(prolog)变量、外部函数以及用于调试、错误处理和格式化的新函数。
变化
5月草案增加了大量新特性,但是我首先讨论对现有特性所做的更改。有些更改是表面上的。 例如,document()输入函数(该函数使用给定的统一资源标识符[URI]返回一个文档)被改为一个新的更短的名字doc()。另外,曾经被写成{- comment -}的注释现在改用新的笑脸符号(: comment :)。是的,现在每个注释都成了一个笑话。
有些更改则是更根本的。也许最重大的改动就是distinct-values()函数不再返回节点(节点是XML结构,如元素、文档、注释以及文本节点),它只返回原子值(如整数或字符串)。尽管该函数仍然接受节点和原子值,但只返回原子值。任何进入该函数的节点都会被"原子化"并被当作原子值,然后以原子形式返回。
原子化的规则很复杂,这里给出一些基本的:由模式定义为布尔型的元素将被原子化为true/false布尔值。定义为整型的元素将被原子化为一个整数。没有被模式定义的元素将被原子化为节点的XPath字符串值(文本节点递归地连接在一起)。
为了说明:
distinct-values(<item>apple</item>,
<item>banana</item>, "grape")
返回值("apple","banana","grape"),假设在模式中没有声明 。在下面的例子中,假如我们假设 被模式定义为布尔型,那么下面的语句:
distinct-values(<status>0</status>,
<status>false</status>)
返回false(),因为它是两个元素的原子值。记住false()是XQuery常量,表示"假"。
现在你也许会想,"当我想返回节点时,可以使用distinct-nodes()函数。" 是的,但该函数只能根据节点标识删除重复节点(那些完全相同的节点,类似于Java中引用的等效节点)。没有能删除等效节点的函数。这会使查询变得复杂,因为没有办法能轻松地删除等效节点。
回过头来看我以前的那篇文章"你所不了解的XQuery",你将发现有些示例会受到这一改动的影响。在那篇文章中,下面的查询返回了艺术家名字的惟一列表,其中每一个名字前后都带有 标记:
distinct-values(document("itunes.xml")
/itunes/Tracks/Track/Artist)
示例输出类似于:
<Artist>Marc Cohn</Artist>
<Artist>Pink Floyd</Artist>
现在,执行同样的查询则返回原子值:
Marc Cohn
Pink Floyd
由于distinct-values()去掉了 标记(这是原子化过程的一部分),所以你必须在完成distinct-values()调用后添加标记,如下所示:
let $artists :=
distinct-values(doc("itunes.xml")
/itunes/Tracks/Track/Artist)
for $a in $artists
return <Artist>{ $a }</Artist>
不是每种情况都是这么轻松地得到处理。看一下W3C 使用案例文档中的示例1.1.9.4在2002年11月版与2003年5月版中是如何变化的。该示例返回每位作者的著作列表。它使用了distinct-values(),根据2002年11月的规范,它的代码如下:
<results>
{
for $a in distinct-values(
document("http://www.bn.com/bib.xml")
//author)
return
<result>
{ $a }
{
for $b in document(
"http://www.bn.com/bib.xml")
/bib/book
where some $ba in $b/author
satisfies deep-equal($ba,$a)
;return $b/title
}
</result>
}
</results>
根据查询结果你不能直接分辨出姓和名,但每个 元素都由一个 和 名组成。distinct-values()调用返回具有惟一名字的 元素列表。对于2003年5月的规范,现在查询必须在姓和名上单独运行distinct-values(),而且在嵌套的FLWOR表达式中也没有将$a指定为惟一的作者:
<results>
{
let $a :=
doc("http://www.bn.com/bib/bib.xml")
//author
for $last in distinct-values($a/last),
$first in distinct-values(
$a[last=$last]/first)
return
<result>
{ $last, $first }
{
for $b in
doc("http://www.bn.com/bib.xml")
/bib/book
where some $ba in $b/author
satisfies ($ba/last = $last and
$ba/first=$first)
return $b/title
}
</result>
}
</results>
除了编写用户定义的distinct-deep-equal()外,没有更好的方法来完成这件事了,而该方法在纯XQuery中不能执行。(注:FLWOR(发音为"flower")表达式是XQuery的构建模块。这个名字来源于组成表达式的要害词For、Let、Where、Order by和Return}。
新函数
2003年5月的XQuery规范草案增加了三个新函数,它们肯定会非常有用。第一个是:
trace($value as item()*,
$label as xs:string) as item()*
trace()函数答应在查询的中间进行printf风格的调试。该函数有两个参数:要显示的值(可以是任意多个项的序列)以及要显示的这个值的字符串标签。为方便起见,函数返回$value传递的值。trace()输出的位置由你的引擎来决定。
该函数使你能够具体查看查询的内部过程。例如,下面的查询根据文档名返回在XQuery引擎中存储的所有文档的URI。通过增加一个trace()调用,我能够在排序前查看返回的每个URI:
define function uris() as xs:string* {
for $n in input()
return trace(
xs:string(document-uri($n)), "base:")
}
for $u in uris() order by $u return $u
输出结果可能如下:
2003-08-01 14:40:46 base: census.xml
2003-08-01 14:40:46 base: ipo.xml
当使用trace()和其他类似的函数时,记住在XQuery中每项内容都是一个表达式。没有语句!为了能够完成类似语句的操作,你可以采用在表达式之间加逗号的方法来创建一个序列。然后,单独计算每个表达式的值。在最终的结果序列中忽略所有返回空值的表达式。例如,下面是两个不影响结果的trace()调用:
trace((), "starting query"),
let $time := current-dateTime()
let $ignored := trace($time, "Got time")
return
<Html>
<head></head>
<body>Current time is { $time }</body>
</html>
请注重第一个trace()调用后面的逗号。它使查询返回一个具有两个项的序列,第一个trace()调用的结果为空,因而被忽略。一般人不了解这一情况,但高级查询实际上就是返回一个序列,因此在这种非凡情况下两端的括号不是必需的。查询"5, "是完全正确的。此外,在这个示例中你会看到,当编写一个FLWOR表达式时,你可以执行let子句右侧的任意代码,并且忽略该值。
5月份的草案还增加了error()函数:
error($srcval as item()?)
这个函数使用户能够报告一个错误,类似于抛出一个异常。$srcval被定义为item()?,意味着它可以是一个XML结构或原子,并且加上问号标记表示这是可选的。下面是一些示例应用:
error()
error("Missing source document")
error(<span>A <i>beautifully</i>
formatted error</span>)
error()调用就像发生异常时那样展开堆栈。遗憾的是,XQuery仍没有try/catch功能。因此,尽管你能抛出错误,但却不能从中恢复。
在5月份的草案中最后一个引人注目的新增函数有一个希奇的名字:round-half-to-even()。它有两种形式:
round-half-to-even($srcval as numeric?)
as numeric?
round-half-to-even($srcval as numeric?,
$precision as xs:integer) as numeric?
在有一个参数的情况下,它的行为类似于round()函数,只是当一个数恰好落在其他两个数的中间时,它将参数取整为最接近的偶数值。数字理论家们会告诉你从统计学上讲这是一个更精确的取整算法。举例说明:
round-half-to-even(1.5) = 2.0
round-half-to-even(2.5) = 2.0
round-half-to-even(2.51) = 3.0
有第二个参数的情况使函数变得很有趣。第二个参数表示精确级,并答应将函数用于格式化小数值。例如:
round-half-to-even(3.567, 2) = 3.57
round-half-to-even(1113.567, -2) = 1100.0
round-half-to-even(1 div 9, 3) = 0.111
在声明中使用的数字数据类型是xs:decimal、xs:integer、xs:float、xs:double以及任何根据限制由它们导出的类型的一种简单表示。它用于XQuery规范中,但你不能在自己的查询中使用它。
新的查询文件结构
2003年5月的草案中最重要的修改包括XQuery文件结构。草案引入了主模块和库模块的概念,并利用它们添加了从可重用组件中构建查询所急需的功能。该草案还扩展了查询序(query prolog),从而提供了大量特性,如可选的版本声明、新的声明、新的导入、变量的外部定义以及函数的定义等。
序类似于(由于边界的限制,有时字符串写在两行上):
module "http://www.w3.org/2003/05/
xpath-functions"
default element namespace=
"http://www.w3.org/1999/xhtml"
declare namespace xs=
"http://www.w3.org/2001/XMLSchema"
import module
"http://www.w3.org/2003/05/
xpath-functions" at "logo.xq"
define function addLogo(
$root as node()) as node()* { }
(: etc :)
通过查看从5月份的规范中借用的巴科斯-诺尔范式(Backus Naur Form ,BNF),我可以描述得更精确一些:
Module::= MainModule
LibraryModule
MainModule::= Prolog QueryBody
LibraryModule ::= ModuleDecl Prolog
ModuleDecl::= "module" StringLiteral
Prolog::= Version? (NamespaceDecl
XMLSpaceDecl
DefaultNamespaceDecl
DefaultCollationDecl
SchemaImport
ModuleImport
VarDefn
ValidationDecl)*
FunctionDefn*
QueryBody ::= Expr
不论你什么时候试图理解一种语言或文件格式的结构,能够阅读BNF都很重要。元符号"::="表示"被定义为",""表示"或",修饰符"?"、"+"和"*"分别表示0或1、1或更多以及0或更多。
该BNF表明每个模块由一个MainModule或一个LibraryModule组成。MainModule是一个后面带QueryBody的序,而LibraryModule则是一个ModuleDecl,其后为一个不带QueryBody的序。ModuleDecl由字符串"module"打头,然后是StringLiteral,本例中它恰好是一个URI。一个序由一个可选的初始版本以及其后的各种声明、导入和多个顺序不限的定义组成。在这之后是数量不限的函数定义。最后,以前在MainModule中引用的QueryBody被定义为一个单一的Expr表达式。Expr以及其他非终结符号的定义可以在规范的其他地方找到。
我在这里说明BNF是因为这是理解新的查询文件结构最准确的方法,并且当有些查询不能像期望的那样运行而不得不求助于规范时,它还会提供极为有用的帮助。
在序中首先应该是Version(版本)。一个XQuery模块可以在序的开始部分声明它的版本号。Version指明了XQuery的版本,根据这个版本设计代码来运行查询。当碰到一个未知的版本时,处理程序可以抛出一个错误。没有版本声明语句则隐含额版本号为"1.0"。遗憾的是,众多预发布的草案不能使用版本,所以该特性现在还不会提供什么帮助。版本声明类似于:
module "http://www.w3.org/2003/05/
xpath-functions"
xquery version "1.0"
(: etc :)
请注重BNF是如何说明版本声明必须永远跟在模块声明后面的。
命名空间
为了理解XQuery的其他组件,首先理解XML的命名空间非常重要,其中包括多数人忽略的一些细节。传统上,XML命名空间答应具有相同本地名(例如 <table>)的元素具有不同的语法含义。命名空间由URI指定,如http://www.w3.org/1999/xhtml指定一个HTML表,http://furnitureworld.com指定一个四条腿的桌子。虽然它们看起来像HTTP URL,但命名空间只是一些含糊的名字。它们与HTTP协议完全无关,而且虽然在Web上的那个URL可能会有一些内容,但并不是一定要有内容!命名空间由前缀"http://"打头只是由W3C开始的,已经成为一个惯例,事实上可以使用任何前缀。
的确,这很令人费解。假如命名空间不是根据URI标准化,如ns:org.w3.1999.xhtml,也许会更轻易理解一些。
XML元素和属性总是与命名空间相关联,即使只是一个不存在的命名空间。非凡的xmlns属性声明一个命名空间前缀别名,用来将元素放到命名空间中。别名在声明xmlns属性的元素的范围内(可用),所有属性和元素都保留在这个元素中。在这个范围内,任何使用该前缀的元素或属性都被认为是在与该别名相关联的命名空间中。例如,下面的代码在命名空间中有三个节点:
<xhtml:table
xmlns:xhtml=
"http://www.w3.org/1999/xhtml"
xmlns:xlink=
"http://www.w3.org/1999/xlink">
<xhtml:tr width="200" xlink:href="#x">
<xhtml:td/>
</xhtml:tr>
</xhtml:table>
表元素声明了两个命名空间别名:xhtml和xlink,它们与两个不同的标准URI相关联。<table>、<tr>和<td> 元素被放在命名空间http://www.w3.org/1999/xhtml中,而href属性被放在命名空间http://www.w3.org/1999/xlin
k中。width属性在前面提到的不存在的命名空间(即第三个命名空间)中。请记住,命名空间的重要组成部分是URI,而不是前缀别名,假如元素的URI相匹配,则它们被认为是在同一个命名空间中,尽管它们的前缀别名可能不同。
"默认的命名空间"是不带前缀的元素使用的命名空间。它由不包括前缀的非凡xmlns属性来分配。默认的命名空间在元素的范围内有效,并请求该元素及其内容,而且会对这个范围内的所有无前缀的元素起作用。但是,默认的命名空间对无前缀的属性不起作用。来看下面这个例子:
<table
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xlink=
"http://www.w3.org/1999/xlink">
<tr width="200" xlink:href="#x">
<td/>
</tr>
</table>
这个XML在语法上等同于前面所示的XML。原来别名为"xhtml"的命名空间现在变成了默认的命名空间,并应用到<table>元素内所有无前缀的元素。因此<table>、<tr>和<td>元素仍在命名空间http://www.w3.org/1999/xhtml中。href属性仍在命名空间http://www.w3.org/1999/xlink中。而且由于无前缀的属性不在默认的命名空间中,所以width属性仍在不存在的命名空间中。
缺少的命名空间可以用空的字符串URI来表示。这样就可以直接放到不存在的命名空间中了。例如:
<table xmlns="http://www.w3.org/1999/xhtml">
<tr> <td/> </tr>
<data xmlns=""> <subdata/> </data>
</table>
这里的<table>、<tr>和<td>元素存放在默认的命名空间即http://www.w3.org/1999/xhtml中,而数据和子数据元素则存放在不存在的命名空间中。
在XQuery中,编写XML代码时你可以使用标准的命名空间声明和规则:
let $bug :=
<x:bug xmlns:x="http://www.bug.com/ns">
<x:desc>Order entry fails</x:desc>
</x:bug>
或者,你可以在序中声明一个命名空间别名:
declare namespace x =
"http://www.bug.com/ns"
let $bug :=
<x:bug>
<x:desc>Order entry fails</x:desc>
</x:bug>
你还可以在序中声明一个默认的元素命名空间:
default element namespace =
"http://www.bug.com/ns"
declare namespace xhtml =
"http://www.w3.org/1999/xhtml"
let $bug :=
<bug>
<desc>Order entry fails</desc>
</bug>
let $report :=
<xhtml:span>{$bug}</xhtml:span>
当使用默认的元素命名空间时,无论是用xmlns属性还是用声明语句声明,你都必须小心。路径表达式会受到它们所处位置范围内的命名空间的影响。在XQuery中,输入和输出命名空间没有区别。要理解为什么会发生这种情况,请先设法确定下面这条查询是否运行:
let $bug :=
<bug xmlns="http://www.bug.com/ns">
<desc>Order entry fails</desc>
<cause>{ input()/bugdb
//item[@id="123"] }
</cause>
</bug>
为了让它运行, 和 元素必须放在哪个命名空间中?id属性呢?答案是,由于路径表达式/bugdb//item[@id="123"]出现在默认元素命名空间http://www.bug.com/ns的范围内,所以在这个命名空间中执行无前缀路径表达式组件。这可能正确,也可能不正确。为了使这个查询能够运行,bugdb和item必须在http://www.bug.com/ns命名空间中。id属性不受默认元素命名空间的影响,因为它是一个属性。前面的查询跟下面这个查询一样:
let $bug :=
<x:bug xmlns:x="http://www.bug.com/ns">
<x:desc>Order entry fails</x:desc>
<x:cause>{ input()/x:bugdb
//x:item[@id="123"] }
</x:cause>
</x:bug>
在生成XHTML内容时经常出现这个问题。Microsoft Internet Explorer浏览器不理解带前缀的XHTML,而是使用具有默认元素命名空间的无前缀XHTML来影响所有在XHTML内容中出现的查询。避免这个问题的一种方法就是在默认元素范围外写一些函数,如下所示:
define function get-body() as element() {
doc("test.xml")/body
}
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
{ get-body() }
</html>
另一种方法是为不存在的命名空间显式地声明一个前缀,如下所示:
declare namespace e = ""
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
{ doc("test.xml")/e:body }
</html>
函数命名空间
XQuery预定义了5个命名空间前缀:
xml
http://www.w3.org/XML/1998/namespace
xs
http://www.w3.org/2001/XMLSchema
xsi
http://www.w3.org/2001/XMLSchema-instance
fn
http://www.w3.org/2003/05/xpath-functions
xdt
http://www.w3.org/2003/05/xpath-datatypes
XML(xml)、XML模式(xs)和XML模式实例(xsi)是XML命名空间和XML模式标准。5月份的XQuery草案引入了最后两个标准--XPath函数(fn)和XPath数据类型(xdt)。
默认函数命名空间是http://www.w3.org/2003/05/XPath-functions,使用标准的"fn"默认前缀。你可以更改这个命名空间,但是你必须使用内置的"fn"前缀来限定任何内置的函数调用。例如:
default function namespace =
"http://example.com/functions"
fn:string-length("foo")
导入模块将函数和变量(也只能是函数和变量)从外部库模块加载到主模块中。在进行模块导入时,你应该指定一个目标命名空间,它需要与模块中声明的命名空间相匹配。这类似于导入一个Java包,其中导入语句路径必须与包语句的路径相匹配。在导入
过程中,你还可以指定加载模块的位置,这个位置以URI的形式给出。任何函数或变量名的冲突都会抛出一个错误。下面是common.xq库模块文件以及随后的main.xq主模块文件:
(: common.xq file :)
module "http://www.w3.org/2003/05/
xpath-functions"
define function uris() as xs:string* {
for $n in input()
return xs:string(document-uri($n))
}
(: no body allowed in library modules :)
(: main.xq file :)
import module
"http://www.w3.org/2003/05/
xpath-functions" at "common.xq"
uris()
common.xq文件(XQuery文件的标准扩展名还没有确定)包含前面给出的uris()函数的定义。利用这个声明,该文件把它的函数放在了http://www.w3.org/ 2003/ 05/XPath-functions命名空间中。main.xq文件用同样的命名空间导入模块。由于uris()函数存放在默认的函数命名空间中,所以main.xq可以调用不带前缀的uris()。
在默认的命名空间中写函数非常方便,而且意味着你不必给你的调用加前缀。但是,high-profile模块应该使用另一个命名空间以避免冲突。我建议像看待Java默认包那样看待"fn"命名空间:它对构造原型很有用,但对已完成的库模块却没什么用途。要想加载带有另一个前缀的模块,可以这样做:
import module namespace x="ns://foo" at "common.xq"
x:uris()
当然,common.xq必须声明同一个命名空间。为了保证导入成功,两边必须一致,就像Java包一样。
请记住,在这些导入中,"common.xq"URI并不是指一个文件路径--它只是一个含糊的名字,服务器将它以某种方式映射为common.xq内容。规范对将URI映射为资源并没有太多强制要求,因为查询不会总存在于文件中。
函数还可以有"外部"声明。外部声明说明函数必须由系统提供。假如系统不提供外部声明,就会出错。这一特性使在外部用非XQuery语言--也许是Java语言编写支持代码成为可能。通过在查询中声明外部函数,引擎的静态分析可以确保函数存在于外部,而且在开始查询前得到正确签名。外部函数在函数体内使用要害字"external"。例如:
define function sort($elts as element()*)
as element()* external
sort((<c/>, <a/>, <b/>))
这个外部函数对一序列元素进行排序。执行排序过程可能是用另一种语言,或由引擎另外提供。向外部环境传递状态或从外部环境传递状态的准确语法目前还没有确定。一旦这些语法被广泛采用,查明到底是Java代码利用XQuery调用更普遍还是XQuery代码利用Java调用更普遍将是非常有趣的。
序变量
你还可以在序中声明和定义变量。假如没有提供变量类型的话,那么变量类型是可选的,并且可以推断出来。值要放在大括号内。例如:
define variable $x as xs:integer {7}
define variable $y {7.5}
(: infer xs:double here :)
在序中声明变量对单独的一个查询没什么意义,但对导入的模块迟早会有用。前面说过导入的模块向主模块提供了函数和变量定义。这使得序变量可以用作全局常量:
module "http://x-query.com/math"
define variable $PI as xs:decimal
{ 3.1415926535897932384626433 }
一个变量还可以被标记为"external",以说明它的值来自外部环境。这就使存储过程参数传递和改变输入源成为可能。例如:
define variable $input
as item()* external
define variable $quantity
as xs:integer external
$input/item[quantity >= $quantity]
结论
2003年5月的XQuery草案引入了一些新的重要特性--从像trace()和error()这样的简单的新实用方法到诸如库模块、函数命名空间、序变量以及外部函数和变量等重要的改动。过去,大多数供给商都要花一些时间来升级他们的XQuery引擎以支持每个新版本,所以你要意识到这里给出的示例代码不一定立即适用于每个引擎。为了帮助你跟踪查询情况,我和Mike Clark创建了一个XQuery测试工具,叫做BumbleBee。该工具类似于XQuery的JUnit。它包含了一组标准测试查询,并使你能够编写自己的查询。对于那些对测试驱动的开发着迷的人来说,BumbleBee是一个非常棒的工
具。你可以先写示例输入和期望的输出,然后写查询。每进行一次升级,就测试一次你的查询,以确保它仍能正常运行。你可以在xquery.com/bumblebee上找到BumbleBee。
Jason Hunter (jasonhunter@servlets.com) )是一名顾问,他是《Java 编程》一书的作者、《Java 企业最佳实践》一书的合著者(这两本书均由O'Reilly & Associates出版公司出版)以及Servlets.com的发行人。
XQJ
在2003年5月的XQuery草案中没有引入但几乎同时公布的是一个来自Oracle和IBM的提议,即创建一个用于XQuery/Java交互的通用API。因为JDBC是用于SQL的,所以这个API将用于XQuery。提议已被提交给Sun公司的Java社区进程(Java Community Process ,JCP)组织,并被该组织接受,作为JSR-225,名为"XQuery API for(用于Java的XQuery API,XQJ)"。该API很可能存在于javax.xml.XQuery包中。
JSR确定的目标中包括:
与JDBC和用于XML处理的Java API(JAXP)在格式上类似;
提供事务性支持的面向连接的界面(因为XQuery 1.0不会有标准的更新机制,所以这一点令人很感爱好);
单次查询的无连接界面;
能够从JDBC连接中创建对引擎有意义的XQJ连接;
能够为重复执行而编译查询;
支持参数化查询和输入参数的发现/绑定;
支持使用JAXP和用于XML的流式API(Streaming API for XML,StAX)处理结果;
T能够处理包括一般序列在内的任何合法结果。
能够使查询结果序列化。
事实终将证实,JSR对Java和J2EE程序员是极为有用的。现在,供给商必须创建用来与XQuery引擎交互的定制的API,只有最好的引擎才会熟悉到结果可以是任意顺序的项,而不是单独的XML文档。使用这个JSR会强化行为的规范性,另外还会提供轻松的后端可插拔性。