内容


跨多个层进行 XML 编程

在中间层使用 XML 以改善性能、提高精确度并简化开发

使用 JDBC 4.0、SQLXML 和 WebSphere Server XML Feature Pack 开发一个纯 XML 解决方案

系列内容:

此内容是该系列 # 部分中的第 # 部分: 跨多个层进行 XML 编程

敬请期待该系列的后续内容。

此内容是该系列的一部分:跨多个层进行 XML 编程

敬请期待该系列的后续内容。

一个样例应用程序:带有数据库集成的博客检查程序

来看下面这个 Web 应用程序:

图 1. 带有数据库集成的博客检查程序
带有数据库集成的博客检查程序的图表
带有数据库集成的博客检查程序的图表

这个 Web 应用程序处理通过博客 Web 服务公开的博客数据。这样的数据包含跟博主的所有博文及博文的所有评论有关的信息,以 Atom XML 形式返回(参见 清单 1 中的示例)。这个 Web 应用程序允许博主跨多篇博文快速查询评论,并删除包含可疑内容的评论。这个 Web 应用程序以 XHTML Web 页面和表单的形式显示数据。如果源数据是 XML 格式且浏览器的数据也是 XML(HTML 或 XHTML)格式,显然应该以原生方式使用 XML 数据。

清单 1. Atom XML 评论提要示例
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text">WebSphere Community Blog</title>
  ...
  <entry>
    <id>tag:blogger.com,1999:blog-1417695962027703953.post-6498982274841848264</id>
    <published>2009-10-17T13:06:00.000-05:00</published>
    <updated>2009-10-17T13:06:00.000-05:00</updated>
    <atom:title xmlns="" xmlns:atom="http://www.w3.org/2005/Atom" type="text">
      Questionable spamming comment title
    </atom:title>
    <atom:content xmlns="" xmlns:atom="http://www.w3.org/2005/Atom" type="html">
      Questionable spamming comment content
    </atom:content>
    ...
    <atom:author xmlns="" xmlns:atom="http://www.w3.org/2005/Atom">
      <atom:name>Joe Smith</atom:name>
      <atom:uri>http://joe.uri.com</atom:uri>
      <atom:email>jsmith@email.com</atom:email>
    </atom:author>
  </entry>
  ...
</feed>

经过一段时间,博主也许会注意到,可疑的评论源自相同的用户或相同的域名(如 清单 1 中的 atom:author 元素所示)。此时,应用程序被更新为允许博主在删除评论时将一个用户或域标记为 “恶意用户”。此信息并不存储在原始评论 Web 服务中,因此需要持久存储在数据存储中。由于此数据已经是 XML 格式,自然应该存储在 XML 数据库中。这允许博主将来使用应用程序来报告关于特定恶意用户的统计数据并就应该删除哪些评论给出建议。

实现这个应用程序的最佳方法是什么?

过去,在数据库对 XML 提供原生支持以前,通常有两种方法来处理 XML 数据。第一种方法是将数据序列化为一个字符串,并将它作为一个字符大对象(CLOB)存储在数据库中,这种方法会引起性能问题,且不允许数据在数据库中作为 XML 查询。

第二种方法是分割数据以映射到一些关系表,这些表粗略地表示 XML 数据的结构。由于关系数据和 XML 数据的表示方法不同,这种方法会引起精确度问题。另外,这种方法还需要用户维护用于映射的代码,并且同样不支持原生 XML 查询。而且,由于新的要求会导致更改 XML 数据架构,所以更改关系映射通常非常繁琐。

鉴于企业中的 XML 数据的不断增长,现在数据库不仅存储关系数据,还开始存储 XML 数据。对于 XML 数据,数据库以原生方式将该数据存储到 XML 列中。支持 XML 列的部分数据库包括 Apache Derby、IBM DB2®、Oracle Database 和 Microsoft® SQLServer。

另外,以前把来自中间层的数据存储到数据库比较困难。在 JDBC 4.0 之前,惟一的选择是使用 String 或 CLOB 数据类型。如前所述,由于需要序列化数据,这些类型会导致性能问题。另外,它们通常需要非标准的 SQL 扩展来理解如何将数据解析到 XML 列中。JDBC 4.0 引入了针对 SQLXML 类型的标准化支持,允许以 XML 形式从(向)数据库读取(写入)XML。JDBC 4.0 允许通过字符串、读取程序和写入程序流或者 JAXP 源和结果而访问 XML 数据。JDBC 4.0 中的这个支持意味着:无需不必要的映射,XML 就可以原生地在中间层和数据库之间流动,这也不会在数据库和中间层导致性能开销。

使用 JDBC 4.0 获取 XML 数据的一个简单示例

接下来详细介绍一个使用 JDBC 4.0 支持的示例。在 JDBC 4.0 中读取 XML 数据类似于处理其他数据类型。

读取 XML 数据的基本步骤如下:

  1. 创建一个预定义语句(prepared statement)。
  2. 执行这个预定义语句获取一个结果集。
  3. 从结果集获取一个 SQLXML 对象。
  4. 通过一个受支持的 get 方法读取这个 SQLXML 对象。
  5. 释放这个 SQLXML 对象。

下面是一个最基本的示例:

清单 2. 使用 JDBC 4.0 读取 XML 数据
PreparedStatement ps =
        dbConnection.prepareStatement("SELECT somexmlcolumn FROM somexmltable");

ResultSet result = ps.executeQuery();

result.next();

SQLXML xml = result.getSQLXML("somexmlcolumn");

StreamSource source = xml.getSource(StreamSource.class);

// Read from the stream source

xml.free();

在本例中,我们通过一个 JAXP 源对象(即 StreamSource)读取数据源。这个源对象允许任何理解 JAXP 源的 API 读取数据。使用 StreamSource 源允许您随意使用任何想用的内存(in-memory)表示。与会引发对象和 API 调用开销的 DOM 或 SAX 等源相比,使用流(stream)是最高效的。

类似地,通过 JDBC 4.0 写入 XML 数据的步骤如下:

  1. 创建一个预定义语句。
  2. 从连接创建一个 SQLXML 对象。
  3. 通过一种受支持的方法访问这个 SQLXML 对象。
  4. 将这个 SQLXML 对象设置为语句参数。
  5. 写入到这个 SQLXML 对象的访问方法。
  6. 执行语句。
  7. 释放这个 SQLXML 对象。

下面是一个最基本的示例:

清单 3. 使用 JDBC 4.0 写入 XML 数据
PreparedStatement ps = dbConnection.prepareStatement(
        "UPDATE somexmltable SET somexmlcolumn = ?");

SQLXML xml = dbConnection.createSQLXML();

StreamResult result = new StreamResult(xml.setBinaryStream());

ps.setSQLXML(1, xml);

// Write to the stream result

ps.executeUpdate();

xml.free();

中间层

如前所述,使用 XML 数据库的价值(相对于 CLOB 或分割为关系数据)是改进性能,提高 XML 精确度,以及简化开发。性能之所以能够得以改进,其原因是数据以单一格式保存,而不是复制到不同的数据模型,后者在情况不佳时可能需要额外的序列化和解析操作。XML 精确度指的是数据库中保存的是 XML 数据的原始版本,而不是从保存 XML 数据的关系表大致重构的版本。之所以能够简化开发,原因是不再需要编写从关系表到 XML 的映射代码。

现在我们来看一下中间层。通过在中间层原生地使用 XML,可在中间层(更重要的是跨中间层和数据库)实现这些好处。另外,除了连接到 XML 数据库之外,中间层还连接到更多的数据源,这个价值能够在其他 XML 数据源(比如 Web 服务提要)之间得到扩展。

端到端性能得到改进,原因是不需要在数据库(或其他 XML 数据源)和中间层之间创建数据副本。每个层上的性能也会得到改进,原因是每个层都能够在内存中创建性能最高的 XML 表示,同时仍旧在数据表示之上提供 W3C 标准编程模型。

精确度也得到维护。在这个简单的示例中,XML 精确度可能是一个小问题。但是,如果正在交换的 XML 文档比较复杂(比如一个税收表单)或比较重要(比如一份财务报告),您就会明白确保 XML 数据在所有的处理框架之间保持其原生格式的重要性了。

最后,在一组统一的 XML 编程模型之间使用单个数据模型能够简化开发。尽管在某些简单场景中,中间层中的 Java™ 编程人员可能可以轻松地将对象映射到 DOM 或 JAXB,但是对于此前提到过的税收表单文档,这样的任务就不会那么简单了。而且,开发人员不仅需要了解 JDBC 和 XML,还需要了解其他编程模型和查询模型。通过处理集合和结果中的 XML 数据,程序员只需了解 XML 数据模型以及用于导航、转换和查询 XML 数据的 W3C 标准。鉴于 XPath 和 XQuery 已经用于在 XML 数据库中进行导航和查询,这些技术可能已经为程序员所熟悉。

简化开发的最简单的方法是:使用一个框架来处理从数据源检索数据、操作数据以及将数据返回数据库等大量低级工作 — 没有创建副本的开销。要在我们的样例应用程序中实现这一点,我们将用到 IBM WebSphere Application Server V7.0 Feature Pack for XML。鉴于 XML Feature Pack 能够以数据库中的原始格式处理 XML 数据,因此不会产生不必要的性能开销。另外,由于数据是 XML 格式,因此 XML Feature Pack 能够轻松导航、转换和查询数据。

IBM WebSphere Application Server V7.0 Feature Pack for XML

IBM WebSphere Application Server V7.0 Feature Pack for XML 引入了对原生 XML 导航、转换和查询的支持,在中间层中使用 W3C 标准 XPath 2.0、XSLT 2.0 和 XQuery 1.0 编程模型。由于 XML 在数据库层很流行,我们想通过下面的基本形式将上述非产品特定的 JDBC 4.0 方法应用于 XML Feature Pack 以及一个 XML 数据库(比如 IBM DB2 with pureXML®、Apache Derby 或 Oracle Database):

图 2. XML Feature Pack 和 XML 数据库的简单拓扑图表
XML Feature Pack 和 XML 数据库的简单拓扑图表
XML Feature Pack 和 XML 数据库的简单拓扑图表

您将看到一个 XML 数据库、SQLXML 的 JDBC 4.0 支持以及 XML Feature Pack 如何支持数据库和中间层中的 XML 数据。

这个场景作为 XML Feature Pack 中的一个带有源代码的样例可用,以便允许您跟随我们的操作,并在您自己的应用程序中进行试验。参考资料 部分提供了下载 XML Feature Pack、Derby 以及 DB2 Express 的链接。

数据库、JDBC 4.0 和 XML Feature Pack 中的原生 XML 支持的结合支持一个直观的高性能架构。

样例应用程序的实现

下面看看如何使用 XML Feature Pack、JDBC 4.0 和一个 XML 数据库实现这个应用程序。首先,使用返回一个 Atom 提要的博客 Web 服务从博客检索恶意评论(参见 图 1)。您处理那些恶意评论时,将检查数据库中的历史信息,看看该评论是否来自此前的恶意博主。自始至终,数据保持 XML 格式。我们首先从 Atom 提要检索关于博文评论的信息。

在 XML Feature Pack 中,来自 Atom 提要的数据通过到那个博客 Web 服务的一个 HTTP 连接加载,并用作一个 XQuery 程序的输入文档。这个 XQuery 程序在 XML Feature Pack 运行时上执行,该运行时使用 XML Feature Pack Java API 调用。

在 XQuery 程序中使用以下 XPath 语句从 Atom 提要检索可疑评论:

清单 4. XPath 语句
declare variable $comments := (
  /atom:feed/atom:entry[atom:author/atom:name = 'Anonymous'] |
  /atom:feed/atom:entry[matches(atom:content, $my:vulgarwords, 'i')])
  [atom:published > current-dateTime() - $my:monthsAgo];

使用 XPath,您在一个用户定义的时间窗口中检索所有由匿名用户发布或包含粗俗词汇的评论。您将所有符合这些标准的条目存储到变量 comments 中。

现在,您希望在 XQuery 中检查该恶意用户是否在数据库中列示为以前曾发表过恶意评论(其中 $i 是上述评论的一个子查询)。

清单 5. XQuery 语句
let $spammedbefore := local:hasEmailHasSpammedBefore($i/atom:author/atom:email/text())

查看这个函数的定义,弄清如何从数据库加载 XML 数据。

清单 6. XQuery 函数
declare function local:hasEmailHasSpammedBefore($emailaddress) as xs:boolean {

let $domainName := substring-after($emailaddress, '@')

return
  if ($domainName = '') then
    false()
  else
    let $jdbcURI := concat('jdbc://getAuthorsWhoHaveSpammedFromDomain?', $domainName)
    let $domainSpammers := collection($jdbcURI)
    return
      not(empty($domainSpammers/spammers/spammer/email[. eq $emailaddress]))
};

我们从电子邮件地址获取域名,然后连接该值和一个已命名查询 jdbc://getAuthorsWhoHaveSpammedFromDomain,生成一个 XPath 2.0 集合 URI:jdbc://getAuthorsWhoHaveSpammedFromDomain?DOMAINNAME

XPath 2.0 集合是集成 XQuery 或 XSLT 程序之外的 XML 数据序列的一种简单方法。集合函数的实现是 “实现定义并依赖于实现的”,即每个 XPath 2.0 运行时能够提供它们自己的默认集合解析器,或者允许用户扩展该运行时以动态提供集合实现。XML Feature Pack 运行时允许用户通过 XCollectionResolver 接口提供它们自己的集合实现。

在这个样例中,我们实现了一个 XCollectionResolver,它处理所有以一个 jdbc:// URI 模式开头的集合,并根据一组预先定义的已命名查询解析该 URI 的余下部分。这个集合解析器查询用户提供的已命名查询,附加位置参数,并针对数据库执行 JDBC 语句。

清单 7 中,我们在定义了一些基本的已命名查询后实例化这个解析器。我们还传递了一个数据库连接到该解析器,以便它能够针对任何 JDBC 连接而工作。

清单 7. 解析器配置
dbStatementsSupportsSQLXML = new HashMap<String, String>();

dbStatementsSupportsSQLXML.put("getAuthorsWhoHaveSpammedFromDomain",
        "SELECT CONTACTS from SPAMMERS where DOMAINNAME = ?");

dbStatementsSupportsSQLXML.put("updateAuthorsWhoHaveSpammedByDomain",
        "UPDATE SPAMMERS SET CONTACTS = ? WHERE DOMAINNAME = ?");

dbStatementsSupportsSQLXML.put("insertAuthorsWhoHaveSpammedByDomain",
        "INSERT INTO SPAMMERS (CONTACTS, DOMAINNAME) VALUES (?, ?)");

Connection conn = getDatabaseConnection();

JDBCCollectionResolver inputResolver =
  new JDBCCollectionResolver(conn, dbStatementsSupportsSQLXML);

清单 8 包含这个解析器实现的一些部分,请参见 XML Feature Pack 样例源代码获取完整的实现。从根本上说,这个解析器与此前展示的那个使用 JDBC 4.0 读取 XML 数据的 简单示例 相同。

为了更加可重用,这个解析器添加了以下内容:

  1. 这个解析器寻找外部提供的已命名查询,而不是直接硬编码 SQL 语句。
  2. 这个解析器检查返回的行和列上的元数据类型,以确保它只读取 SQLXML 类型的 XML 列。
  3. 最后,这个解析器使用 XML Feature Pack API 的其他两个构造体(XSequenceCursorXItemView)来构造一个从 JDBC 查询返回的 XML 数据序列。
清单 8. 解析器实现
public XSequenceCursor getCollection(String uri, String base) {
        // look up query from query collection provided from Listing 7
        String query = lookupNamedQuery(uri);
        PreparedStatement p = dbConnection.prepareStatement(query);
        ResultSet rs = p.executeQuery();
        ...

        // Loop through the result returned from the query
        ResultSetMetaData metadata = rs.getMetaData();
        int colType = metadata.getColumnType(jj+1);
        if (colType = Types.SQLXML) {
                SQLXML sqlx = rs.getSQLXML(...);
                StreamSource source = sqlx.getSource(StreamSource.class);
                XItemView item = itemFactory.item(source);
                sqlx.free();
        }

        // Use the XML Feature Pack API to create a sequence from the returned XML data
        ...
        XItemView itemView[] = items.toArray(new XItemView[0]);
        XSequenceCursor sequence = itemFact.sequence(itemView);

        return sequence;
}

此时,我们拥有一个将在任何 XQuery 程序中使用的非常常规的集合解析器,它接受任意数量的输入,并返回将通过 XPath 2.0、XSLT 2.0 或 XQuery 1.0 处理的 XML 数据集合。这里还有两个示例。在第一个示例中,我们使用 name 元素创建了一个所有恶意用户列表;在第二个示例中,我们返回发布了 10 条以上评论的恶意用户的数量。

清单 9. 更多 XQuery 示例
Java:
  dbStatementsSupportsSQLXML.put("getAllSpammers", "SELECT CONTACTS from SPAMMERS");
XQuery:
  let $allSpammers := collection('jdbc://getAllSpammers')
  return
    for $i in $allspammers
    let $first := $i/name/first
    order by $i/name/last
    return
      <name>
        <first>{ $first }</first>
        <last>{ $i/name/last }</last>
      </name>

Java:
  dbStatementsSupportsSQLXML.put("getAllSpamAuthorsWhereSpamCountGreaterThan",
    "SELECT CONTACTS from SPAMMERS where COUNT > ?");
XQuery:
  let $minCount := 10
  let $allSpammers := collection(concat(
    'jdbc:// getAllSpamAuthorsWhereSpamCountGreaterThan?',
    $minCount)
  )
  return
    count($allSpammers)
}

另外,由于集合解析器是 XPath 2.0 的一部分,它对于 XSLT 2.0 与对于 XQuery 1.0 同样有用。下面是使用集合解析器的一个简单 XSLT 2.0 示例:

清单 10. 更多 XSLT 示例
<xsl:variable name="allSpammers" select="collection('jdbc://getAllSpammers')"/>

<xsl:template match="/">
        <p>The current spammer database contains the following domains and spammers.</p>
        <xsl:for-each select="$allSpammers">

        <table>
        <tr>
                <th>Name</th><th>Email</th>
        </tr>
        <xsl:for-each select="$allSpammers">
        <tr>
                <td><xsl:value-of select="name"/></td>
                <td><xsl:value-of select="email"/></td>
        </tr>
        </xsl:for-each>
        </table>
</xsl:template>

类似地,也可以将数据写入一个 XML 数据库。XSLT 2.0 允许使用 xsl:result-document 指令将结果写入通过一个 URI 标识的多个文档。如前所述,这个 URI 的解析是依赖于运行时实现的。为允许用户指定结果写入的地方,XML Feature Pack 提供了 XResultsResolver 接口。在本例中,我们实现了一个类似的 JDBC 结果解析器,它通过以一个哨兵值 —XML— 标示的 XML 参数使用几个已命名语句和位置参数。

这允许将数据写入数据库,如 清单 11 所示:

清单 11. XSLT 结果-文档
<!--  Is this an insert or an update -->

<xsl:variable name="insert" select="count($spammersByDomain/spammers/spammer) eq 0"/>

<!-- Create the insert statement named query -->

<xsl:variable name="insertJdbcURI"
        select="concat('jdbc://insertAuthorsWhoHaveSpammedByDomain?--XML--&', $domain)"/>

<!-- Create the update statement named query -->

<xsl:variable name="updateJdbcURI"
        select="concat('jdbc://updateAuthorsWhoHaveSpammedByDomain?--XML--&', $domain)"/>

<!—
If insert, insert the xmldoc into the database.
Otherwise, update the database with the xmldoc.
 -->

<xsl:template match="/">
        <xsl:when test="$insert">
                <xsl:result-document href="{$insertJdbcURI}" method="xml" indent="yes"> 
                        <xsl:copy-of select="$xmldoc"/>
                </xsl:result-document>
        </xsl:when>
        <xsl:otherwise>
                <xsl:result-document href="{$updateJdbcURI}" method="xml" indent="yes">
                        <xsl:copy-of select="$xmldoc"/>
                </xsl:result-document>
        </xsl:otherwise>
</xsl:template>

当您在出现新的恶意用户(一个特定的域没有任何恶意用户存在)的情况下执行这个代码时,xsl:when 指令的插入路径将执行,这时,XML 运行时将收到一个请求,请求将 xmldoc 变量的内容写入一个结果 URI 中:

清单 12. 结果 URI
jdbc://insertAuthorsWhoHaveSpammedByDomain?--XML--&domain.com

这个样例的结果解析器然后将这个语句解析为一个我们预先定义的已命名查询:

清单 13. 已命名查询
INSERT INTO SPAMMERS (CONTACTS, DOMAINNAME) VALUES (?, ?)

这个样例的结果解析器然后将变量 xmldoc 的内容附加到第一个位置参数,将 domain.com 附加到第二个位置参数中。

与使用集合读取 XML 数据一样,使用结果写入 XML 数据并不是某种 XML 语言所特有的。在 上一个写入示例 中,XSLT 用于该示例(参见 清单 11)。使用相同的结果解析器方法,可以指示一个 XQuery 1.0 程序将输出写入一个 XML 数据库。

使用一个通用 JDBC 解析器与使用其他方法的对比

这里介绍的方法 —— 在通过 JDBC 4.0 实现的 XML Feature Pack 中定义 JDBC 输入和输出集合以及结果解析器 —— 只是实现 XML Feature Pack 和一个 XML 数据库之间的自然的高性能集成的一种方法。

另外,也可以将输入(集合解析器)实现为一个用户定义的、更特定的 XPath 2.0 扩展函数,如下所示:

清单 14. 更特定的 XQuery 函数
declare function local:hasEmailHasSpammedBefore($emailaddress) as xs:boolean {

let $domainName := substring-after($emailaddress, '@')

return
  if ($domainName = '') then
    false()
  else
    let $domainSpammers := my:getAuthorsWhoHaveSpammerFromDomain($domainName)
      return
        not(empty($domainSpammers/spammers/spammer/email[. eq $emailaddress]))
};

这种方法的好处是:输入和输出的定义更严格。这种方法的缺点是:必须为每个查询编写一个扩展函数(而不是为所有可能的查询只编写一个)并且只对输入有效(在所有情况下为输出编写一个扩展函数不自然,也不可能)。使用更常规的集合和结果解析器能够以一种自然统一的方式处理大多数情况。

有一点值得注意:本文讨论的集合解析器和结果解析器是 XML Feature Pack 样例代码的一部分。要在一个生产应用程序中使用该代码,请您根据您的应用程序的需求扩展样例代码。

本文故意简化了 XML 程序,以便本文简单易懂。需要提醒的是,样例应用程序可以通过使用 XML Feature Pack 样例代码运行,且所有(完整的)源代码均已包括。

结束语

如前所述,使用一个 XML 数据库(而不是 CLOB 或分割为关系数据)的价值是改进性能、提高 XML 精确度和简化开发。性能之所以能够得以改进,其原因是数据以单一格式保存,而不是复制到不同的数据模型,后者在情况不佳时可能需要额外的序列化和解析操作。XML 精确度指的是数据库中保存的是 XML 数据的原始版本,而不是从保存 XML 数据的关系表大致重构的版本。之所以能够简化开发,原因是不再需要编写从关系表到 XML 的映射代码。

通过在中间层原生地使用 XML(使用 XML Feature Pack),可在中间层(更重要的是跨中间层和数据库)实现这些好处。另外,除了连接到 XML 数据库之外,中间层还连接到更多的数据源,这个价值能够在其他 XML 数据源(比如 Web 服务提要)之间得到扩展。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Information Management, WebSphere
ArticleID=489230
ArticleTitle=跨多个层进行 XML 编程: 在中间层使用 XML 以改善性能、提高精确度并简化开发
publish-date=05122010