内容


在 Java 环境中使用 XQuery

在 Java 应用程序中使用 XQuery 搜索文档

Comments

SQL 数据库、XML 数据和查询

虽然编程领域 — 特别是 Java 编程 — 不断发展,可供选择的标准的数量也在增长。换句话说,越来越多的 API 得到 Sun 的认可或者批准。标准化的结果是越来越多的开发者背弃了自己最具竞争力的技术,而去学习新技术。

要掌握的最有吸引力和价值的是那些和数据处理有关的工具和 API。无论应用程序多么酷或者智能,说到底只有能够处理数据才有用。同时,虽然 API 数量的不断增长,但流行和通用的数据格式数量却不断减少。虽然有些数据管理员仍然使用面向对象的数据库管理系统或者 XML 驱动的数据库,但关系数据库(RDBMS)已成为主流,而且仍然是大部分数据管理员的选择。因此 Java 开发人员必须通过 JDBC(数据库连接)或者 JDO (Java Data Objects) 与 SQL 数据库交互。

数据库之外的数据基本上也都以 XML 作为标准数据格式。XML 虽然冗长但是健壮,Java 语言中处理 XML 的 API 可能比其他语言都多。无论是解析、数据绑定还是转换,如果应用程序不能处理 XML,就会被认为有局限性甚至有点落后。

两者看似无关,— 数据往往保存在 SQL 数据库中,数据库之外的数据越来越多地采用 XML 格式 —,但是却造成了一些独特的问题。SQL 数据库容易查询,而 XML 文档则不是这样。消费者希望能够方便地搜索数据,数据库中的数据查询方便,但是 XML 文档中的数据就不那么容易了。显然,将 XML 格式的数据塞到数据库中以方便搜索的做法是行不通的。于是就出现了 XQuery — 相应的也就有了 XQuery API for Java (XQJ)。

XQuery:三种技术

简言之,XQuery 就是用于定义 XML 文档搜索的一种语言。就像 SQL 赋予了 SELECT 和 FROM 特定的含义一样 — 在一定的上下文中 — XQuery 定义了正斜杠(/)和地址符号(@)以及其他一些关键字和关键字符的意义。

XQuery 的核心包括三部分:

  1. XPath 规范:在 XML 文档中选择零个、一个或多个节点的方法。
  2. 选择特定 XML 文档、为 XPath 返回的节点增加选择条件的附加语法
  3. API — 比如 XQJ,XQuery for Java API— 用具体的编程语言对 XQuery 表达式求值

要真正精通 XQuery,必须切实掌握好这三个方面。对于 Java 程序员来说,显然就意味着要学习 XPath、学习新增的 XPath 语法结构,然后将其与基于 Java 的 API 结合起来对 XML 文档使用 XQuery 表达式。

好消息是 XPath 和 XQuery 语法都非常简单明了。如果曾经使用 UNIX® shell、Mac OS X 终端或者 DOS 窗口研究过目录结构,就具备了必要的基础。掌握了小于(<)、大于(>)和等于(=)这些运算符的基本用法外,您称得上是大半个 XPath 专家了。

XPath 基础

事实上,XQuery 差不多完全依赖于另一个 XML 规范,即 XPath 规范。XPath 的作用基本上就是定义创建指向 XML 文档某一部分的路径 的方法。比如,XPath /play/act/scene 就是指 play 根元素下的 act 元素下的所有 scene 元素。

XPath 是相对路径

最基本的 XPath 使用元素名和正斜杠。默认情况下,XPath 以 XML 文档中的当前位置开始。因此如果使用 DOM,比如导航到 speech 元素,然后指定路径 speaker,则指向当前位置的 speech 元素中的所有 speaker 元素。因此 XPath 的求值是相对于文档中的位置而言的。

从当前节点移动

要移动到文档的根元素,可在路径前加上正斜杠。无论在文档的什么位置,/play 可以查找名为 play 的根元素。用 ../ 可以选择当前元素的父元素。应该能够看出这实际上非常类似与目录结构。路径 ../../personae/title 将从当前元素上溯两层,然后依次寻找 personae 和嵌套在其中的 title 元素。

选择属性

除了元素外还有很多可供选择的其他内容。比如这个 XPath:/cds/cd@title。它返回根元素 cds 下 cd 元素上所有的 “title” 属性。

要记住,@ 返回的不是属性的值而是属性本身。因此,@isbn 将选择所有名为 isbn 属性而不是这些属性的。此外,XPath 中的属性指的是属性名和属性值(请参阅后面的 关于节点 一节)。

选择文本

就像选择元素和属性一样,也可选择元素中的文本。如果 XPath 以元素名结束,比如 /cds/cd/title,则选择的是元素 — 而且 包括这些元素中的文本。如果需要元素中的文本,可使用 text() 语法。因此要取得所有 CD 的文字标题,可以使用 /cds/cd/title/text() 这样的路径。该路径不提供任何元素,而是指定元素中的文本。

XPath 选择一组节点

有效使用 XPath 的一个关键是要认识到 XPath 始终计算的是节点集。这个集合可能包含零个、一个或多个节点,但 XPath 的结果必定 是集合。多数人在编写 XPath 的时候认为:该路径返回一个元素、一个属性或者文本,但事实并非如此。不过这也不是绝对的。

如果使用 DOM,应该已经对节点有所了解了。对于 DOM 来说,XML 文档中的一切 — 元素、属性和文本 — 都是节点。元素有元素节点,属性(包括属性值)有属性节点,元素中的文本则是文本节点。因此路径 ../../personae/title 最终将选择 title 元素,实际上返回一个节点集。这个集合可能包含零个(没有匹配的元素)、一个或者多个节点。集合中的所有节点都是名为 “title” 的元素。

随着路径变得更加复杂,有可能选择一个更大的集合 — 也许同时包括属性和元素,或者同时包括文本和元素 — 无论如何这些路径最终都是选择一个节点集。牢记这一点是正确使用 XQuery 的关键。通过 XPath 选择一个节点集,而使用 XQuery 则通常是按照一定条件选择这些节点的一个子集,也有可能连接多个节点集然后应用搜索条件。只要记住集合中的元素可能有多种类型(元素、文本或者属性),就能更好地编写路径并保证得到预期的结果。

增加 XPath 的选择性

前面我们看到了如何根据节点名(元素和属性)以及父节点(文本或者选择给定节点的所有子节点)来选择节点集。这本身已经非常强大了,不过 XPath 还提供了更多的选择性,即使用所谓的谓词

谓词语法

谓词是用于已有节点集的部分表达式。谓词放在方括号 [] 中。作用于谓词左侧的路径所定义的节点集。比如路径 /cds/cd 选择了根元素 “cds” 中的所有 cd 元素。假设需要第一张唱片,可以使用谓词来实现:/cds/cd[1]。它返回路径 /cds/cd 选择的第一个节点。

谓词可用于任何节点集

要记住,谓词作用于谓词本身左侧的节点集。但是这并不意味着谓词只能出现在 XPath 的最后面。可以将 XPath 本身看作是路径的集合,每个路径都返回一个节点集,路径后面的部分作用于该集合并进一步细化。因此 /cds/cd/title 实际上是三个路径:

  1. /cds 返回根元素 “cds”(只含一个元素节点的集合)
  2. cd(相对于上一个节点集)返回上一节点集中嵌套的所有 cd 元素
  3. title(同样相对于上一节点集)返回上一节点集中嵌套的所有 title 元素

谓词必须作用于节点集,但是可作用于任何 节点集。/cds[1]/cd[2]/title[1] 这样的路径是完全合法的。它选择了 /cds 所选节点集中的第一项,/cds[1]/cd 所选节点的第二项,/cds[1]/cd[2]/title 所选节点的第一项。

注意:该路径中有些部分毫无意义,比如使用 / 选择根元素然后再使用 [1] 谓词总是返回集合中的第一个(惟一的一个)元素。只有当节点集本身为空,即给出的根元素名称和文档实际的根元素不同,这种情况下才不会返回任何元素。但是作为一个例子 — 包括从技术的角度看 — XPath 本身以及使用的谓词都没有问题。

超越谓词中的数值索引

当然,只能用数值引用位置的 API 作用是很有限的。这样的模型中,您必须知道需要的项到底在什么位置。不过 XPath 提供了更多的选择。首先,在谓词中可以使用 last() 函数选择集合中的最后一项,不论集合中包含多少项。/cds/cd[last] 选择文档中最后一个 cd

使用 position() 函数还可以选择在特定位置之前或者之后的所有项。position() 函数返回给定节点在集合中的位置。比如,假设需要前 5 张唱片,可以使用路径 /cds/cd[position()<6]。这样就能选择 position() 结果小于 6 的所有节点。

根据数据选择节点

最后 — 对于这里关于 XPath 的简要介绍而不是详细讨论来说 — 还可以根据节点的子元素或者节点的属性来选择节点。就像 XPath 后面的部分依赖于前面路径所决定的节点集一样,集合的谓词也依赖于所应用的集合。谓词除了能够使用小于(<)和大于(>)这样的运算符外,还可根据选中节点的数据而不仅仅是这些节点在整个集合中的位置进行选择。

比方说要选取属性 “style” 为 “folk” 的所有 CD。表达式应该首先选择全部 CD,然后把这些 CD 的 style 属性和 “folk” 进行比较。其 XPath 形式为 /cds/cd[@style='folk']。按照前面的说明,这个表达式的意思应该很清楚了。首先,通过 /cds/cd 选择了一个节点集。然后使用谓词 @style 取出每个节点的 “style” 属性。前面已经提到,@ 表示属性。并且该属性是关于已经选择的节点集的(即所有 cd 元素)。接下来把这些属性的值和字符串 “folk” 比较。属性匹配的返回,其他的则在结果集中排除掉。

该方法也可用于所选集合的嵌套元素。假设文档结构如清单 1 所示。

清单 1. CD 清单文档的结构
<cds>
 <cd style="some-style">
 <title>CD title</title>
 <track-listing>
 <track>Track title</track>
 <!-- More track elements... -->
 </track-listing>
 </cd>
 <!-- More CDs... -->
</cds>

比方说如果希望取得包含 10 条或更多音轨的 CD。首先要选择全部 CD 元素,即前面多次用到的路径:/cds/cd。然后使用谓词得到关于所选集合的特定节点集的项数,即嵌套在需要返回的节点集之中的 track-listing 元素下所包含的 track 元素节点的个数。最后还需要对这些节点计数,XPath 提供了 count() 函数。此外还需要把这个数字与 10 比较。于是得到了路径:/cds/cd[count(track-listing/track) >= 10]

关于 XPath 谓词中的矛盾

如果阅读得足够仔细,可能会注意到 XPath 对元素文本和属性的处理是不一致的。前面,我提到属性节点包括属性及其值作为一个信息单位来处理。但是在谓词中,@type 这样的表达式引用的不是整个 type 属性节点而仅仅是属性值。从而能够与其他值进行比较(如 @type='reggae')。

同样,也可在谓词中引用元素文本,如 /cds/cd[title='Revolver']。这里用嵌套在 cd 中的 type 元素的值和 “Revolver” 比较。而且和属性节点一样,违背了看待元素的几条标准规则。一般来说,元素节点是文本节点的父节点,但这里谓词实际上引用了元素中的文本。

这种轻微的规则违背不是大问题,只要您知道有这种情况,而且能够转换对元素和属性的两种不同思考方式即可。只需要明确什么时候将属性及其值看作一个节点,什么时候引用属性值;类似的,也知道什么时候元素包含其他文本节点,什么时候文本值能够和其他值比较。

看看 XQuery

XPath 无疑非常强大,但也有其局限性。最突出的是,它很大程度上只适合静态数据。换句话说,需要针对特定文档编写 XPath 查询,提供和元素、属性、文本比较的具体数据来使用谓词和 XPath。此外,XPath 也没有任何控制结构(如 if/else 语句),除了简单的比较外也不能执行任何处理。

坦白地说,这些限制对多数非程序员来说算不上大问题。但是对于 Java(或者 C#、Python)程序员,习惯了完整的编程语言的强大功能,很快就想到需要 XPath 本身功能之外的方法搜索 XML 文档。于是 XQuery 理所当然地登场了。

选择文档

XQuery 中很少使用但是非常重要的一个特性是能够指定应用 XPath 的文档。前面我们已经将路径 /cds/cd[title='Revolver'] 应用到了一个特定的文档,但在 XQuery 中可使用 doc() 函数指定文档。因此如果搜索 catalog.xml,可使用 XQuery 表达式 doc("catalog.xml")/cds/cd[title='Revolver']

这个小函数的好处在于可以编写代码来以编程的方式选择文档(比如根据用户的输入),或者遍历一组文档(比如网络中所有的 iTunes 目录)并分别应用该语句。

XQuery 和 FLWOR

当然除了简单地文档选择外,XQuery 还提供了更多的功能。它的 FLWOR 功能尤为强大。FLWOR 是 “for、let、where、order by、return” 的缩写。这都是可用于 XQuery 表达式以便得到更精确结果的子句。

对于 SQL 老手来说,应该感到有些熟悉了。 WHERE 和 ORDER BY 都是 SQL 查询中常见的部分。对于程序员来说, for 应该比较眼熟。下面是关于 FLWOR 子句的简要说明:

  • for:使用 for 遍历节点集。在很多方面,for 就将节点集的当前值赋给变量从而操作该变量。
  • let:使用 let 可以为变量赋值,但是(很快将看到)和其他 FLWOR 子句相比,let 用的比较少。
  • wherewhere 允许向节点集应用选择条件,除了 XPath 原有的机制意外。当然,您将看到在很多查询中 where 并不比 XPath 强,只不过把 XPath 的谓词转移了一个地方。
  • order byorder by 子句不改变或者筛选数据,仅用于排序结果集,XPath 只能按位置排序,该子句允许按照其他数据排序。
  • return:使用 return 子句允许您操作操作节点集,随后返回除该节点集以外的结果。有可能在选择节点集、排序并筛选之后,只返回结果的子元素,return 是实现这种功能的关键。

我们再稍微深入地看看这些子句。

使用 for 子句

for 子句的用法和 Java 和 C# 中的用法基本相同。其格式如下:

for $variable-name in XPath
...

其中的变量名可以是任何一般的标识符,如 x。变量一般最好根据用途命名(如 firstName 或r title),不过由于这个变量基本上是一个循环计数器,也可使用单个字母。

XPath 没有限制。/cds/cd 是一个很好的例子。再比如:

for $cd in doc("catalog.xml")/cds/cd
...

如此而已。因此变量 $cd 将取得 XPath /cds/cd 返回的每个节点的值。其中的 ... 代表 XQuery 表达式的其他部分,后面将讲到。

对于程序员来说,这个语句实际上和下面的语句没有区别:

for (int i = 0; i<cdArray.length; i++) {
 CD cd = cdArray[i];
 // Process CD
}

或者像列表那样:

for (Iterator i = cdList.iterator(); i.hasNext(); ) {
 CD cd = (CD)i.next();
 // Process each CD
}

使用 let 为变量赋值

先暂缓上面对 XQuery 的讨论,let 子句用于为变量赋值。您可能已经看到,XQuery 中通过在标识符前加上美元符号($)来定义变量。多数情况下按照上述方法使用 XQuery 中的变量,即通过 for 子句隐式建立变量,而不是使用 let 子句显式地定义变量。

不过也有可能需要使用显式的变量。应该这样做:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
...

这里将搜索的文档赋给一个变量。XQuery 中的赋值使用 :=— 除非曾经用过 Pascal— 可能有点奇特。如果在更真实的环境中,可能用一个函数迭代遍历一组 XML 文档,依次将这些文档名赋给 $docName。这样就能选择每个 文档中的每个 cd 元素,按照同样的方式分别处理。

用 return 子句完成查询

为了完成查询,我们先跳过 FLWOR 中的排序子句。现在我们已经得到了:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
...

接下来就要返回一些东西了。查询选择了所有 cd 元素,但是返回这些元素没有多少用处 — 虽然这就是需要的节点集。这里不返回这些元素,我们假设需要某种更容易识别的内容,比如存储在 title 元素中的每张 CD 标题。这就用到 return 了。

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
return $cd/title/text()

首先选中了所有的 cd 元素。然后依次将得到的节点赋给 $cd。最后, return 子句返回的不是元素本身,而是包含目标数据的子元素 “title”。

一定不要 犯下面的错误:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
return /cds/cd/title/text()

这里有三个明显的问题:

  • 首先它忘记了 XQuery 的目的,返回了一个 XPath 路径而没有执行任何查询。
  • 其次,没有使用 doc($docName) 返回数据,该变量指定了要从中选择 CD 的文档。
  • 最后,也是最重要的,该表达式将忽略对 for 子句返回的节点集所执行的筛选或排序操作。

后面还要用到这样的查询,其重要性将更加明显。目前,先要记住必须保证 for 子句定义的变量同时出现在 return 子句中。这一简单的法则可以保证查询得到预期的结果。

用 where 选择

where 增强了 XQuery 的选择能力。XQuery 中的 where 子句和 SQL 中一样,在选择中增加 where 子句是为了进一步细化结果集。这是一个非常简单的例子:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
where $cd@type = 'reggae'
return $cd/title/text()

该查询返回所有瑞格舞曲唱片的标题。不需要进一步解释,where 子句非常简单。通过 and 还可建立更加复杂的条件:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
where $cd@type = 'reggae'
 and count($cd/track-listing/track) > 10
return $cd/title/text()

返回包含 10 个曲子以上的所有瑞格唱片。

另一种重要的 where 应用类型是执行连接。比如有一个清单 2 所示的 XML 文件。

清单 2. 扩展后的 CD 清单文档
<cds>
 <cd style="some-style">
 <title>CD title</title>
 <artist id="289" />
 <track-listing>
 <track>Track title</track>
 <!-- More track elements... -->
 </track-listing>
 </cd>
 <!-- More CDs... -->

 <artists>
 <artist id="289">
 <firstName>Bob</firstName>
 <lastName>Marley</lastName>
 </artist>

 <!-- More artist elements -->
 </artists>
</cds>

它扩展了 清单 1 所示 XML 文档的结构。增加了 artist 元素,该元素用 id 属性标识,每张 CD 至少有一个 artist 元素嵌套在 cd 元素中。

使用 XQuery 可以实现唱片和艺术家的连接。比如:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
 $artist in doc($docName)/cds/artists/artistwhere $cd/artist/$id = $artist/$id
 and $artist/lastName = 'Marley'
return $cd/title/text()

这里有两点需要注意。您第一次看到 for 子句可以定义多个变量。除了 CD 外,该语句还定义了 $artist,从而处理 artist 元素集合。

其次,where 子句使用 where $cd/artist/$id = $artist/$id 连接了 CD 和艺术家。记住,这样将匹配每张 CD 和每位艺术家,从而得到相当于 SQL 连接的效果。然后进一步选择:$artist/lastName = 'Marley'。从而得到姓氏为 “Marley” 的所有艺术家。但这同时是一个连接,return 子句返回 CD 标题。从而得到了姓氏为 “Marley” 的艺术家的所有唱片的标题。

这正是 XQuery 独有的功能。可以对 XML 文档(很多可能没考虑到进行高级搜索)执行复杂的、类似 SQL 的连接和选择。

排序节点集

如果说 where 和对应的 SQL 结构非常 相似,那么 order by 就是和对应的 SQL 结构完全 相同了。可以根据任何能够通过 XPath 引用的数据对结果排序:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
 $artist in doc($docName)/cds/artists/artist
where $cd/artist/$id = $artist/$id
 and $artist/lastName = 'Marley'
order by $cd/release/@date
return $cd/title/text()

这里返回的 CD 标题按照每张 CD 的子元素 releasedate 属性排列。默认情况按升序排列,如果愿意也可明确说明:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
 $artist in doc($docName)/cds/artists/artist
where $cd/artist/$id = $artist/$id
 and $artist/lastName = 'Marley'
order by $cd/release/@date ascending
return $cd/title/text()

当然也可按照降序排列。下面的查询首先返回最新发行的唱片:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
 $artist in doc($docName)/cds/artists/artist
where $cd/artist/$id = $artist/$id
 and $artist/lastName = 'Marley'
order by $cd/release/@date descending
return $cd/title/text()

注意:如果没有模式或者使用的 XQuery 处理程序不知道 date 属性是日期类型,该语句可能会出现错误。更糟的是,有可能把日期作为文本数据按照字母顺序来排序。不过基本上所有的现代处理程序都能识别日期,因此很少出现这种情况。

排序还可以包括更多的条件。比如,上面的表达式返回姓氏为 “Marley” 的所有 艺术家(不仅仅是 Bob)的唱片,按照发行日期排序的话不同艺术家的作品就会混在一起。可以进一步改进该表达式,首先按名字然后按发行期排序:

let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
 $artist in doc($docName)/cds/artists/artist
where $cd/artist/$id = $artist/$id
 and $artist/lastName = 'Marley'
order by $artist/firstName, $cd/release/@date
return $cd/title/text()

请注意,首先按 $artist/lastName 然后按 $artist/firstName 排序没有必要,因为结果中的姓氏都一样。

引入 Java 技术

为了在 Java 环境中使用 XQuery,前面的介绍似乎太罗嗦了。但是,选择 XQJ(常见的 Java XQuery API 缩写)的多数程序员至少要对 XPath 和 XQuery 有所了解。现在我们介绍的东西比基础稍多一点,可以在 Java 程序中使用这些表达式了。

XQuery 规范是供应商中立的

XQuery for Java API 是在 Sun 的支持下作为 Java Community Process, JSR 225(链接见 参考资料)的一部分开发的。规范本身涉及到多个不同的供应商(包括 Sun、Nokia、BEA、Oracle、Intel 等)和一些知名人士(比如 Jason Hunter,servlet、JDOM 和 XML 方面的名人)。因此,能够避免维系于特定的数据库供应商或者 XML 产品厂家。

但 XQJ 的实现不是中立的

不幸的是,还没有 XQJ 的 Sun 的标准实现。专家组的多数供应商都致力于在其产品中提供 XQJ 实现,这就意味着必须解决某些和特定供应商有关的问题。当然,对于长期使用 XML 的人来说,这和二十世纪初的 XML 解析器与 XSL 处理程序之争没有什么区别。随着时间的推移 XQJ 将标准化,Sun 几乎肯定会发布自己的 XQJ 版本或者 XQJ 实现的包装器 API,就像 XML 解析器与 XSL 处理程序的 JAXP 一样。

获得 XQJ 实现

学习 XQJ 最简单的办法是从 DataDirect 下载免费的试用版。必须填写一份非常烦人的表单,不过此后就可以使用足够长的时间 — 至少能用到本文结束。请访问 DataDirect XQuery 下载站点(链接见 参考资料)。还必须进入需要访问的数据库 — 即使选择 XML Documents Only 选项。配置好这些选项之后,就会得到一封电子邮件说明到哪里下载 JAR 文件 datadirectxquery.jar

展开 JARred JAR

安装过程有点麻烦,首先需要解压下载的 datadirectxquery.jar。可以使用 jar 命令。但首先要建立安装目录,然后将 JAR 文件解压到该目录。运行 jar 命令:

[bdm0509:~/Desktop] mkdir xqj
[bdm0509:~/Desktop] cd xqj
[bdm0509:~/Desktop/xqj] jar xvf ../datadirectxquery.jar 
 inflated: XQueryInstaller.jar
 inflated: ddxqj.jar
 inflated: ExtensionTool.jar
 inflated: Readme.txt
 inflated: 3rdPartySoftware.txt
 inflated: Fixes.txt
 inflated: installer.properties

运行安装程序

现在打开新建的目录并双击 XQueryInstaller.jar 文件。如果系统中安装了 Java,将打开 GUI 安装程序。

提示您选择试用版还是注册版,选择 trial。接下来需要选择安装目录。输入的目录一定要具有写文件的权限。我选择的是 /usr/local/java/xqj,首先要保证能够写入 /usr/local/java 目录。安装过程还将建立一个子目录 — 该例中的目录名为 xqj— 然后将 DataDirect XQuery 文件放入该目录。最后运行安装并单击 Finish

完成之后打开新建目录看看其中的内容,应该如下所示:

[bdm0509:/usr/local/java] cd xqj
[bdm0509:/usr/local/java/xqj] ls
3rdPartySoftware.txt examples lib
Fixes.txt help planExplain
Readme.txt javadoc src

将 XQJ JAR 添加到类路径

现在需要修改类路径,使其包含 XQuery JAR 和存储在 lib 目录下 ddxq.jar 中的 XQJ 类。这不是 原来下载的 JAR,而是在上面解压过程中由 JAR 安装的(如果 DataDirect 采用 ZIP 或 .tar.gz 压缩文件可能就不那么容易混淆了)。可以手工设置类路径,也可使用 shell 脚本、.profile 文件或者使用 IDE 将 JAR 添加到类路径中。要保证类路径中能够访问到 ddxq.jar

建立数据库连接

DataDirect 下载也提供了数据库连接功能,从而能够对关系数据库运行 XQuery。填写下载表单的时候,必须选择(可能)连接的数据库。然后 DataDirect 定制下载以便能够设置数据库。这超出了本文的讨论范围,但是如果除了对磁盘上的 XML 文档使用 XQuery 外,也对使用 DataDirect 库连接到数据库感兴趣,可以查看 参考资料 小节的有关链接。安装的 lib 目录中还有其他很多 JAR 文件,但是简单的文件查询用不到这些文件。如果以后使用 DataDirect 数据库连接,也可研究一下这些 JAR。

在 Java 中运行 XQuery

掌握了 XPath 和 XQuery 并且类路径中有了 XQJ 实现之后,就可以编写 Java 代码运行查询了。每个程序都包含两个基本的步骤:

  1. 建立/访问 XQuery 数据源。
  2. 执行 XQuery。

这两个步骤都很简单,而且只要不改变 XQuery 实现,在所有程序中第一步都是一样的。实际上可以将处理数据源配置和连接的代码打包成一个工具类(留给读者作为练习)。

处理 XQuery 数据源

如果熟悉 JDBC 或者编写过 n-层数据驱动的应用程序,您可能已经对数据源的概念非常熟悉了。在这里,数据源就是一个连接对象,如何连接以及连接到哪里都被抽象化了。因此,数据源可能是到 MySQL 数据库的网络连接,也可能是到静态 XML 文档的基于文件的连接。一旦建立了数据源,就可以操作它而不用关心连接的语义了。

如果只需要查询本地磁盘上的 XML 文档(也是本文的重点),连接的设置非常简单。清单 3 中的简单 Java 程序建立了可以查询的数据源。

清单 3. 建立查询数据源
package ibm.dw.xqj;

import com.ddtek.xquery3.XQConnection;
import com.ddtek.xquery3.XQException;
import com.ddtek.xquery3.xqj.DDXQDataSource;

public class XQueryTester {

 // Filename for XML document to query
 private String filename;

 // Data Source for querying
 private DDXQDataSource dataSource;

 // Connection for querying
 private XQConnection conn;

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

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

 public static void main(String[] args) {
 if (args.length != 1) {
 System.err.println("Usage: java ibm.dw.xqj.XQueryTester [XML filename]");
 System.exit(-1);
 }

 try {
 String xmlFilename = args[0];
 XQueryTester tester = new XQueryTester(xmlFilename);
 tester.init();
 } catch (Exception e) {
 e.printStackTrace(System.err);
 System.err.println(e.getMessage());
 }
 }
}

看起来很长和复杂。主要是因为该测试程序按照非常模块化的方法编写 — 使用构造函数 init() 方法 — 以便不需要很大修改就能用于您自己的程序。

该程序取得文件名(从命令行获得并传递给类的构造函数),然后执行下面的代码:

dataSource = new DDXQDataSource();
conn = dataSource.getConnection();

首先建立一个新的数据源,该对象的类型是 com.ddtek.xquery3.xqj.DDXQDataSource。由于没有连接到数据库,不需要其他配置,因此使用了空构造函数。然后使用数据源得到新的 com.ddtek.xquery3.XQConnection 对象。该对象对于建立和执行新的 XQuery 表达式非常重要,虽然现在还不能执行这些功能,但是可以接受查询字符串并执行了。

查询真正的 XML 文档

还需要一个进行实际查询的 XML 文件。下载 小节包含一个压缩的 CD 目录文件,W3C 提供这个 XML 文档就是为了作为例子。该文件有 200 多行,这里不再列出来了,对于本文中的查询足够了。

清单 4 显示了该文档的一小部分,以便您能了解其结构。

清单 4. cd_catalog.xml 的一部分
<?xml version="1.0" encoding="ISO-8859-1"?>
<CATALOG>
 <CD>
 <TITLE>Empire Burlesque</TITLE>
 <ARTIST>Bob Dylan</ARTIST>
 <COUNTRY>USA</COUNTRY>
 <COMPANY>Columbia</COMPANY>
 <PRICE>10.90</PRICE>
 <YEAR>1985</YEAR>
 </CD>
 <CD>
 <TITLE>Hide your heart</TITLE>
 <ARTIST>Bonnie Tyler</ARTIST>
 <COUNTRY>UK</COUNTRY>
 <COMPANY>CBS Records</COMPANY>
 <PRICE>9.90</PRICE>
 <YEAR>1988</YEAR>
 </CD>
 <!-- more CD listings ... -->
</CATALOG>

建立 XQuery

接下来需要创建实际查询。使用 Java String 就能做到。创建一个新变量保存查询字符串,如下面的 XQueryTester 类所示(这些代码在 main() 方法中):

 try {
 String xmlFilename = args[0];
 XQueryTester tester = new XQueryTester(xmlFilename);
 tester.init();

 final String sep = System.getProperty("line.separator");
 String queryString =
 " for $cd in doc($docName)/CATALOG/CD " +
 " where $cd/YEAR > 1980 " +
 " and $cd/COUNTRY = 'USA' " +
 " order by $cd/YEAR " +
 " return " +
 "<cd><title>{$cd/TITLE/text()}</title>" +
 " <year>{$cd/YEAR/text()}</year></cd>";
 System.out.println(tester.query(queryString));
 } catch (Exception e) {
 e.printStackTrace(System.err);
 System.err.println(e.getMessage());
 }

查询选择 1981 年以后的所有采用 US 格式的 CD,按照发行年份排列,返回包含每张 CD 标题和年份的 XML。有几个地方需要注意:

  • docName 变量表示要搜索的文档。代码需要使用由命令行传递给程序的文档来填充该变量。
  • 返回的不是结果集中每个节点的值,而是一个 XML 字符串。XML 可以放入另外一个更大的 XML 文档、在线显示或者传递给 XSL 处理程序。
  • 源文档中的 XML 元素名 —CDTITLEYEAR 等 — 在结果集中都不见了,改用新的 XML 元素名:cdtitleyear

这些查询本身都很简单,只不过从另一个方面证明了 XQuery 从 XML 选择和返回数据的灵活性。

值得一提的是,如果结果集返回 XML 字符串,应该用花括号将返回字符串中的变量包围起来:

return <cd><title>{$cd/TITLE/text()}</title>" +
 " <year>{$cd/YEAR/text()}</year></cd>"

花括号告诉 XQuery 处理程序要将包围起来的数据作为需要求值和替换的变量而不是文字文本。

声明外部变量

XQuery 使用变量 $docName 表示文档。但是需要显式声明该变量并告诉查询外部程序 — 这里就是 Java 程序 — 将定义该变量。在 XQuery 中需要使用 declare 语法。declare 语法格式如下:

declare variable [variable-name] as [variable-type] external;

该例中的 [variable-name]$docName[variable-type] 应该是 XML Schema 基本数据类型。多数时候,字符串使用 xs:string,整数使用 xs:int。还有其他几种类型,但这两种最常用。

因此对于 XQueryTester 类需要修改查询:

 try {
 String xmlFilename = args[0];
 XQueryTester tester = new XQueryTester(xmlFilename);
 tester.init();

 final String sep = System.getProperty("line.separator");
 String queryString =
 "declare variable $docName as xs:string external;" + sep +
 " for $cd in doc($docName)/CATALOG/CD " +
 " where $cd/YEAR > 1980 " +
 " and $cd/COUNTRY = 'USA' " +
 " order by $cd/YEAR " +
 " return " +
 "<cd><title>{$cd/TITLE/text()}</title>" +
 " <year>{$cd/YEAR/text()}</year></cd>";
 System.out.println(tester.query(queryString));
 } catch (Exception e) {
 e.printStackTrace(System.err);
 System.err.println(e.getMessage());
 }

现在只需要编写一个执行查询的函数。

运行 XQuery

运行查询需要经过以下步骤:

  1. XQConnection 创建一个 XQExpression 对象。
  2. 使用 XQExpression 对象的 bindXXX() 方法把变量绑定到查询。
  3. 执行查询,结果保存到 XQSequence 对象中。

这些步骤中只有绑定变量需要一些技巧。该例中的变量 docName 需要和传递给程序的文件名联系起来。绑定字符串变量需要使用 bindString。该方法有三个参数:

  1. javax.xml.namespace.QName 实例(JAXP 包中的一个类)和 XQuery 中的变量名
  2. 绑定到变量的值
  3. 变量类型应该匹配的原子类型

结合起来得到的方法应该是:

public String query(String queryString) throws XQException {
 XQExpression expression = conn.createExpression();
 expression.bindString(new QName("docName"), filename,
 conn.createAtomicType(XQItemType.XQBASETYPE_STRING));
 XQSequence results = expression.executeQuery(queryString);
 return results.getSequenceAsString(new Properties());
}

第一行 XQExpression expression = conn.createExpression(); 建立了一个新的表达式对象。然后,查询的文档文件名绑定到 docName 变量:expression.bindString(new QName("docName"), filename, conn.createAtomicType(XQItemType.XQBASETYPE_STRING));。现在不用考虑 QName 对象的机制,只需要把 XQuery 中的变量的名称(没有 $ 字符)赋给它即可。下一个参数是文件名;第三个是一个非常固定的值,要求数值所符合的类型。由于查询中变量被定义为 xs:string,因此需要 XQItemType.XQBASETYPE_STRING 类型。

虽然有些代码不够直观,大部分只要查看 API 文档(链接见 参考资料)就明白了。其他 bindXXX() 方法如 bindInt()bindFloat() 等等,都是相同的原理。

最后执行查询并返回结果序列。可以迭代结果集,或者直接将其转化成字符串让程序处理。query() 方法就是这么做的,因而不需要调用程序了解 DataDirect XQuery API。

清单 5 显示了完整的 XQueryTester 代码。

清单 5. 完整的 XQueryTester 类
package ibm.dw.xqj;

import javax.xml.namespace.QName;
import java.util.Properties;

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 XQueryTester {

 // Filename for XML document to query
 private String filename;

 // Data Source for querying
 private DDXQDataSource dataSource;

 // Connection for querying
 private XQConnection conn;

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

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

 public String query(String queryString) throws XQException {
 XQExpression expression = conn.createExpression();
 expression.bindString(new QName("docName"), filename,
 conn.createAtomicType(XQItemType.XQBASETYPE_STRING));
 XQSequence results = expression.executeQuery(queryString);
 return results.getSequenceAsString(new Properties());
 }

 public static void main(String[] args) {
 if (args.length != 1) {
 System.err.println("Usage: java ibm.dw.xqj.XQueryTester [XML filename]");
 System.exit(-1);
 }

 try {
 String xmlFilename = args[0];
 XQueryTester tester = new XQueryTester(xmlFilename);
 tester.init();

 final String sep = System.getProperty("line.separator");
 String queryString =
 "declare variable $docName as xs:string external;" + sep +
 " for $cd in doc($docName)/CATALOG/CD " +
 " where $cd/YEAR > 1980 " +
 " and $cd/COUNTRY = 'USA' " +
 " order by $cd/YEAR " +
 " return " +
 "<cd><title>{$cd/TITLE/text()}</title>" +
 " <year>{$cd/YEAR/text()}</year></cd>";
 System.out.println(tester.query(queryString));
 } catch (Exception e) {
 e.printStackTrace(System.err);
 System.err.println(e.getMessage());
 }
 }
}

运行 XQueryTester

编译该程序并运行:

[bdm0509:~/Documents/developerworks/java_xquery] 
 java ibm.dw.xqj.XQueryTester cd_catalog.xml 
<cd><title>Greatest Hits</title><year>1982</year></cd><cd><title>
Empire Burlesque</title><year>1985</year></cd><cd><title>When a man loves a woman
</title><year>1987</year></cd><cd><title>The dock of the bay</title><year>
1987</year></cd><cd><title>Unchain my heart</title><year>1987</year></cd>
<cd><title>Big Willie style</title><year>1997</year></cd><cd><title>
1999 Grammy Nominees</title><year>1999</year></cd>

注意:为了适应在线文章的格式插入了分行符。实际的结果没有断行,而是连成一行。

实验!

该程序能够处理查询和绑定到 XML 文档。现在可以尝试不同的查询字符串,体会 XQuery 和从 Java 程序中运行这些查询。尝试选择所有的唱片,将返回结果的格式改为格式化文本。看看如何返回所有的唱片,从最新的到最老的,或者价格低于 10 美元的所有唱片。有了这样的程序后,很容易就能修改查询以便进行试验,甚至改变提供给程序的 XML 文档。

结束语

不了解 XQuery 就很难讨论 XQJ,不熟悉 XPath 就很难讨论 XQuery。将这些不同的部分结合起来通常意味着仅仅处理其中的一部分— Java 程序执行查询,或者开始查询的文档中的位置 — 非常简单。这恰恰也是在 Java 中使用 XQuery 的最佳途径:这些简单的、独立的部分结合起来能够组成强大的程序。

由于必须熟悉多种不同的技术,尝试一个方面的时候应该保持其他两个方面不变。先保持 Java 程序不变,尝试不同的查询和不同的输入文档吧。然后从一个简单的查询开始,尝试在查询中使用更多的输入变量。或者改变返回语句中的 XPath,然后再修改搜索的起点。等这些部分都熟悉了之后,就会发现您的查询更复杂,Java 代码更健壮。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Java technology, Information Management
ArticleID=310504
ArticleTitle=在 Java 环境中使用 XQuery
publish-date=05292008