|
开发人员
XML
XQuery的诀窍和陷阱(一)
作者:Jason Hunter
在XQuery系列的第三部分,了解如何避免常见的XQuery错误。
在以前的两个关于XQuery的系列文章中,我为读者介绍了这种设计用于查询XML数据集合的强大语言。(参见Oracle杂志2003年5/6月刊的"你所不了解的XQuery"和11/12月刊的"更新XQuery"两篇文章。)
为帮助您更好地了解如何正确地构成XQuery查询,在这篇文章中,我将重点放在XQuery语言中那些重要的、同时又需要较高技巧并且通常会导致误解的方面。我要提醒大家的是,
除这点外,事情将会变得非常复杂。但是就这些复杂的方面先给你一些直接的警告总要比当你在查询失败时不得不自己推导一切要好。本文基于2003年5月的XQuery规范草案。
输入函数
编写一个好的查询的第一步就是选择合适的输入。XQuery具有三个输入函数来提供对后台存储数据的查询访问。(后台存储数据是指XML文档集,XQuery可以根据这个文档集执行查询。)第一个函数input()返回输入序列,通常是一个对应于数据库中所有可用文档的文档节点序列。在以前的文章中,你可以看到该函数用来输出所有存储文档的URI:
for $n in input() return document-uri($n)
input()函数通常以一个XPath表达式开始。例如,下述代码会返回后台存储数据库中任何文档内的所有图书元素:
input()//book
XQuery规范没有制定检查输入序列的明确规则。这就使得特定环境返回任何结果对其状态都会产生影响。在关系环境中,返回的可以是从一个SQL查询检索出来的文档;在另一个环境中,input()函数可以返回前一个查询的直接结果,从而实现了查询链接。
这种方法有一个缺点,input()函数标记返回一个节点序列(节点是XML的构造元素,包括元素节点、文档节点、注释节点和文本节点等),然而查询却能够返回一个项目序列(项目可以是一个节点或原子值,如xs:interger或xs:string)。因此,input()并不能完全表现任意查询的结果。
第二个输入函数collection($uri as xs:string)返回一个对应于给定URI的节点序列。事实上,每个URI如何分解为一套节点取决于服务器及其配置。该函数允许服务器管理员预先选择一套要查询的文档。服务器管理工具能够将文档映射到已命名的集合。与input()函数一样,该函数通常也是返回文档节点,但是其标记允许函数返回任意类型的节点,因此它对于不同环境来说具有一定的灵活性。下面的查询返回来自任意抵押赖账账户的地址元素:
collection("deadbeats")//address
最后一个输入函数是doc($uri as xs:string),它返回与给定URI名相关的文档节点。与collection()一样,该函数规定了URI到节点的映射。下面的查询从一个特定的文档中取出一个<part>元素:
doc("inventory.xml")/parts/part[@id="54"]
在使用多个doc()输入函数查询一套通用文档前,一定要检查服务器的配置(或询问服务器管理员),看看通用集合是否已经被映射到URI,以便在collection()中使用。
序列
input()函数和collection()函数都返回序列。为了在XQuery中充分利用序列,你必须非常了解系列的工作原理及其特殊的特性。每个序列都有一个顺序(类似于Java列表,而不是Java集合),并且允许有任意数量的项目--包括空值,即空序列,用()来表示。
下述表达式用一个原子值和一个元素节点表示了一个序列:
(1, <elt/>)
序列中可以有相同的值:
(1, 2, 2, 3)
但是序列绝对不能嵌套。任意嵌套都会被自动取消,例如:
(10, (1, 2))
等价于
(10, 1, 2)
(1 to 3, 1 to 2)
等价于
(1, 2, 3, 1, 2)
也许令人很惊讶的是,一个项目竟然与包含该项目的、长度为1的序列一样。这使你可以将一个单一项目传递给支持序列的函数:
1 = (1)
你可以通过使用操作符union、intersect和except将节点序列组合起来。union操作符(也被写作
| )将两个序列连接起来;intersect 操作符返回在每个序列中产生的所有节点;except操作符返回在第一个而不是第二个序列中产生的所有节点。注意,union、intersect和except操作符只对对节点序列起作用,对项目则不起作用。这意味着你不能将这些操作符用于原子值。
如果你想查询两个集合的联合,你可以参照如下代码:
(collection("a")|collection("b"))//elt
作为另外一个例子,下面的表达式产生的搜索结果是基于两个用户定义的搜索函数对一个源$list的操作。查询计算两个标准的"and"和"or"结果,以及所有符合第一种标准却不符合第二种标准的结果。它只返回空序列,以使表达式完整。
let $x := match-condition-one($list)
let $y := match-condition-two($list)
let $andResult := $x intersect $y
let $orResult := $x union $y
let $xorResult :=
$orResult except $andResult
return ()
union操作符为XQuery提供的巨大SQL优势之一是,能够处理不同的输入。文档格式会随着时间而变化--细微或者显著的结构变化,取决于文档的来源和文档生成的时间。使用union操作符,单个的查询可以处理多种输入模式,如下所示:
input()/root/(title|titl|head/title)/text()
如果序列的交集非空,那么序列就被认为是相等的。这就产生了一些非常令人吃惊的结果!
(1, 2, 3) = 1
("pink", "floyd") = ("pink", "posies")
尽管这种行为违反常规,但在其他环境中,这种比较方式却显得相当直观。例如,如果任何书的作者名字叫做Hunter
,那么测试语句 $book/[author = "Hunter"]应该而且确实会返回真(true)值。
当你想对两个序列进行全面比较时,你可以使用deep-equal()函数。该函数类似于Java的equals()方法,用来判断任意两个参数是否相等。下面是该函数的签名(signature):
deep-equal($param1 as item()*,
$param2 as item()*) as xs:boolean
该函数对序列或单个项目(因为单个项目就是长度为1的序列)起作用。当对XML元素节点进行比较时,deep-equal()函数会比较它们的名称、命名空间、属性和文本,并且能够递归比较其子元素。其中的注释和处理指令都被忽略。下列代码返回的结果为真:
deep-equal(
<elt a="b">
<!-- first -->
<child>c</child>
</elt>,
<elt a="b">
<!-- second -->
<child>c</note>
</elt>)
deep-equal()调用需要花费一定的时间来处理大的元素节点。在某些情况下,使用只根据节点特性来比较节点的快捷函数会更好(也就是说,这些节点其实是完全相同的节点,而不只是相等,类似于Java中的==操作符)。这个快捷函数是sequence-node-identical()。下面的代码返回真值,因为这些完全相同的节点以相同的顺序存在于每个序列中:
sequence-node-identical(
($elt1, $elt2), ($elt1, $elt2))
但是下面的代码返回假(false),因为创建了两个不同的节点:
sequence-node-identical(<elt1/>, <elt1/>)
下面的查询可快速判断两个集合是否具有一致的定义:
sequence-node-identical(
collection("a"), collection("b"))
数据和字符串值
XQuery的数据模型已经成为混乱之源。部分原因是它自身的复杂性,部分原因则是为使查询更简单而做的努力导致特殊实例的数据相当大。有时,在XQuery中处理数据类型的所有操作看起来都是不可思议的。现在我来解释一下这种不可思议性。
在XQuery中的项目有类型值和字符串值。类型值始终是原子值。类型值的示例包括xs:integer
5、xs:string "foo"、 xs:double 500.1等所有用于标准XML模式的类型。而字符串值则只是一个简单的xs:string。
你可以通过调用data($seq)函数来计算某一项目的类型值。然而,你很少能直接调用该函数,它通常是在当XQuery引擎执行需要将一个节点当作一个原子值的操作时,被XQuery引擎隐含调用的。例如,当你调用$track/PlayCount
* $track/TotalTime时,在技术上你其实是将节点相乘了,但是这样做也能够得到结果,因为XQuery引擎间接使用了它们的类型值。
元素的字符串值通常是其所包含的递归连接的文本。下述元素具有字符串值"It's true
he sold 10 units。" 你可以通过string($elt)函数来检查这个值:
<elt>It's <stmt>true</stmt> he sold <count>10</count> units.</elt>
一个元素的数据值依赖于它的XML 模式类型。如果上述元素<count>具有xs:integer模式类型,其数据值将会是xs:integer
10,如果该元素具有xs:double模式类型,其类型值将会是xs:double 10.0;如果该元素具有xs:string模式类型,其类型值将会是xs:string"10"。
布尔元素的作用与上述元素类似。如果上述<stmt>元素具有xs:boolean模式类型,其数据值就会是xs:Boolean
true()。
如果没有可用的模式又会怎样?如果是这样,那么元素的数据值就会与其字符串值一样,但这只是特殊类型xdt:untypedAtomic的一个实例。"xdt"前缀指出它是XQuery数据类型的一部分,它是由XQuery规范引入的。根据不同的使用方法xdt:untypedAtomic类型可以转换并且可以作为字符串或数值。因此,如果<count>不是类型元素(untyped),那么$count="10"将为真,而且$count=10也将为真。
对于一个具有复杂内容的元素(如<elt>)来说,没有类型值,只有字符串值。调用data($elt)将会产生一个错误。
布尔值
每个项目和序列都有一个"有效的布尔值"。你会看到该值经常被使用,尤其是在类似于下面的where子句中:
for $c in input()//item/count
where $c
return $c
这里的where子句是where$c,并且$c被绑定到一个<count>元素。它如何工作?规则是:如果where子句的值如下,那么一个项目或序列具有有效的布尔值false():
- 空序列
- 布尔false()
- 空字符串
- 数字0
- 双精度型/浮点型NaN
否则,它的值就是true()。可以通过boolean($seq)函数来确定一个项目或序列的布尔值。下面是一些真/假题(答案会在后面给出):
- boolean(1)
- boolean((1))
- boolean(0)
- boolean((0))
- boolean((0, 0))
- boolean(())
- boolean(string(<foo/>))
- string(boolean(<foo/>))
- boolean(string(data(<foo/>)))
- boolean(string(data(<foo>x</foo>)))
- boolean(data(<foo>false</foo>))
答案是:
- true(),因为该值不符合任何假条件。
- true(),因为这是一个长度为1的序列,因此它与第一题中的值是一样的。
- false(),因为它相当于数字0。
- false(),同样,也是因为它具有与第三题中一样的值。
- true(),因为它不符合任何假条件。
- false(),因为这是一个空序列。
- false(), 因为字符串值是"",并且符合假条件。
- true, 因为boolean()调用了一个不符合任何假条件的元素,而且true()的字符串值为真。
- false(), 因为<foo/>的数据是"",类型是xdt:untypedAtomic,它的字符串值是"",它的布尔值为false()。
- true(), 因为<foo>x</ foo>的数据是x,类型是xdt:untypedAtomic,它的字符串值是x,它的布尔值为true()。
- 不定。在没有模式的情况下,<foo>的类型值是假,类型是xdt:untypedAtomic,如果不符合任何假条件,它的布尔值就为true()。
通过一个模式将<foo>指定为类型xs:boolean,其类型值为false(),有效的布尔值也为false()。
现在我们来回顾一下最初的<count>示例,看看是否能够判断它会返回怎样的结果。
for $c in input()//item/count
where $c
return $c
答案:如果<count>具有XML模式类型xs:integer,就会返回每一个内容非零的<count>。如果<count>具有类型xs:double或xs:float,它就会返回每个内容为实际数字(与NaN这种特殊的非数值量相反)的<count>。如果没有特定的模式类型,它就会返回任何具有任意文本内容的<count>。另外,如果<count>具有复杂的内容(属性或子元素),它就会产生一个错误。
|
测试优先的学习
BumbleBee XQuery测试工具为你提供了一个了解XQurey的技术难点的便利工具。通常用来测试全部生产查询的测试工具还提供了一个环境,用于试验、探索和记录从生产环境中分离出来的行为。测试实例文件类似于知识库,以文件的形式记录如何执行(或者说是应该如何执行)查询。测试不仅帮助你了解和预测行为,而且通过运行对新版本的XQuery引擎的测试,你还可以通过生产查询检查是否引入了可能导致问题的任何消极因素。你可以在本文的"下一步"部分找到更多有关BumbleBee的信息。
|
在实际应用XQuery时,最好不要假设每个人都了解这些规则。为了更明确,可以在直接返回一个布尔值而不是隐含一个有效的布尔值的where子句中使用一个表达式。如果表达不明确就可能会给你带来麻烦。例如,在早期的文章中,你可能会调用下述查询代码:
define function star-count($movie-id) {
let $review-doc :=
doc("movie-reviews.xml")
return avg($reviewdoc/reviews/review
[movie-id = $movie-id]/stars)
}
let $movie-doc := document("movies.xml")
for $movie in $movie-doc/movies/movie
let $stars := star-count($movie/@id)
return
<movie id="{$movie/@id}">
{$movie/title}
<stars>{
if ($stars) then $stars else "N/A"
}</stars>
</movie>
实际上这里有一个bug,你发现了么?本来期望if子句对任意电影返回N/A,因为没有存储任何影评,所以star-count()函数返回空序列。然而,如果star-count()函数返回xs:integer
0,那么它也能返回N/A,因为if子句认为有效的布尔值false()成立。在本例中,用if (exists($stars)) then
$stars else"N/A"表达会更好。
排序
你按FLWOR子句指定的顺序进行排序时,XQuery数据模型与我们密切相关。你能否在下面的查询语句中找到应该返回按价格排序的图书的bug?
for $b in doc("bib.xml")/bib/book
order by $b/price
return $b
这里的bug就是该查询执行了一个词汇排序操作,而不是数字排序操作。价格将以129.95、39.95和65.95的顺序排列。在一个词汇排序中,任何以1开头的数字总是会排在以3开头的数字前面。之所以会发生这样的情况,是因为排序是基于节点的类型值进行的。
假设bib.xml没有一个模式来通知XQuery引擎<price>元素是xs:decimal类型,那么,<price>元素将有一个类型值--字符串值xdt:untypedAtomic。
你可以通过指定一个模式或者在命令中包含一个强制类型转换(cast),按如下语句进行排序:
for $b in doc("bib.xml")/bib/book
order by xs:decimal($b/price)
return $b
尽管很奇怪,但是下述查询却运行良好,并且返回一个数字平均价格:
avg(doc("bib.xml")/bib/book/price)
该操作能够成功是因为avg()具有一个特殊的规则,就是能自动将xdt:untypeAtomic值转换为数字。
第二部分:XQuery的诀窍和陷阱(二)
|