John A. Bocharov
Microsoft Developer Network
2000年9月
摘要: 本文概述了使用 SQL XML 重新实现的 Duwamish Online 分类浏览,SQL XML 是创建数据驱动页面的一种新型 Web 技术。
目录
简介
Microsoft® SQL Server™ 2000 引入了一套名为 SQL XML 的新技术。SQL XML 可以直接从数据库服务器获得 XML,并通过 Internet Server API (ISAPI) 接口访问数据库。对于 Duwamish Online,这意味着:基于 Duwamish Books, Phase 4 n-层设计(英文)的 Duwamish Online 分层体系结构包含五个物理层,其中距离数据库最远的—表示层—为 XML 所驱动。SQL XML 通过促进应用程序端对端地使用 XML,为 Web 应用程序开发提供了一种新模型。
作为对这一新技术的实验,我使用 SQL XML 技术套件提供的新体系结构重新实现了 Duwamish Online 分类浏览 — 替换了 Duwamish Online 数据库以上的所有层次。最终的应用程序 Duwamish Online SQL XML 复现了所有原来的功能,并对 Duwamish Online 中的许多层次执行了全部相同的逻辑操作。本文概述了各种 SQL XML 功能在 Duwamish Online SQL XML 中的应用,以及在此过程中获得的经验教训。从体系结构观点来说明 Duwamish Online SQL XML 的文章将在 2000 年 11/12 月的 MSDN 新闻中发表。(请在 11 月初在线查找该文。)
SQL XML 工具
SQL XML 体系结构允许两个物理层:Web 接口层和数据库层。Web 接口层处理数据访问、业务逻辑、工作流程和表达操作。为了使代码模块化和功能分层,该体系结构可以集成 XDR 方案、模板和 XSL 样式表。为了优化性能,Web 接口层运行于 SQL XML ISAPI 应用程序,该应用程序在功能上与 Active Server Pages (ASP) 引擎类似,并为 HTTP 数据库访问专门进行了高度优化。由于数据缓存、数据访问和多种数据转换已经自动化,因此新的体系结构允许大规模地缩减开发时间和代码量。不过,自动化的程度可以进行灵活的控制,从而使开发人员能够更好地控制最终结果,同时避免编写大量低级代码。
模板
模板为可以通过 URL 访问的 XML 指令文件。在 Duwamish Online SQL XML 中,模板执行工作流程操作。它们以 URL 中指定的参数作为输入,然后输出 XML 或 HTML 块。下面示例为 Duwamish Online SQL XML 中生成分类页的模板子集。
<!-- 命名域声明为所有模板文件所必需 -->
<ROOT xmlns:sql="urn:schemas-microsoft-com:xml-sql" >
<sql:header>
<!-- sql:param 标记指定模板的一个输入参数。
该参数在 URL 中以标准查询字符串格式(例如
http://myserver/myvroot/templates/cat.xml?PKId=829)指定。
该参数的值则存储于变量 @PKId -->
<sql:param name = "PKId">
</sql:param>
</sql:header>
<!-- sql:query 标记封装一个 SQL 查询。该查询调用一个存储过程,存储过程的运行速度要快
于未编译的 ad hoc 查询。-->
<sql:query>
exec GetCategoryForXML @PKId
</sql:query>
</ROOT>
该模板可以通过访问下列 URL 执行:
http://myserver/myvroot/templates/cat.xml?PKId=829&contenttype=text/xml
其中 myserver 为服务器名称,myvroot/templates 为模板文件所在的路径,cat.xml 为模板。
注意 有关声明命名域的详细信息,请参阅 http://msdn.microsoft.com/xml/xmlguide/namespaces-declaring.asp(英文)。
请注意 contenttype 参数。由于该参数为 ISAPI 应用程序所保留,因此并没有在模板中声明。其值应与目录一致以确保正确显示页面。(模板还可以用于显示 HTML— 未来将有更多内容。)本示例中生成 XML 的逻辑没有显示,该逻辑封装在由模板引用的 GetCategoryForXML 存储过程中。生成的 XML 如下所示:
<ROOT xmlns:sql="urn:schemas-microsoft-com:xml-sql" >
<Categories CatId="829">
<Category ItemId = "830" ItemName = "Anthropology">
<Product ItemId = "439" ItemName = "African Genesis" />
<Product ItemId = "413" ItemName = "Cannibals and Kings" />
</Category>
</Categories>
</ROOT>
请注意,在前面的代码中为了清晰起见而忽略了某些属性、产品和分类。
XSL 转换
所幸的是,模板的功能不只局限于检索 XML。只需少许修改模板,SQL XML ISAPI 应用程序将自动检索并在结果中应用 XSL 样式表。在 root 标记中添加如下代码:
<ROOT xmlns:sql = "urn:schemas-microsoft-com:xml-sql" sql:xsl="mystyle.xsl" >
要确保页面正确显示,请务必在 URL 中添加 "contenttype=text/html"。前面用于访问模板的 URL 可作如下修改:
http://myserver/myvroot/templates/cat.xml?PKId=829&contenttype=text/html
如果样式表使用 XSLT,还可以通过 <xsl:output> 标记实现同样的效果。
下一步,让我们看一些检索 XML 数据的方法。每一种方法均可在模板中以指令方式使用。
XDR 方案
XML Data Reduced (XDR) 方案简化了数据库信息到 XML 的转换。XDR 方案本身为 XML 文档,提供强大的语法将数据库中的表和列映射为输出 XML 文档中的元素和属性。但是,这项技术的根本优势在于能够使用 XML 路径语言 (XPath)(请参阅 http://www.w3.org/TR/xpath [英文])语法来检索指定信息子集。XPath 提供用来定位 XML 文档中指定元素的语法。通过 XDR 方案可以将数据库看作是一个巨大的 XML 文档,Xpath 查询只选择文档中的一小部分返回给用户。当然,出于性能方面的考虑,并不会生成如此巨大的 XML 文档。
XDR 方案在模板中以 <sql:xpath-query> 标记引用。下面是 detail.xml 的一个版本,即生成 Duwamish Online SQL XML 项明细页面的模板:
<ROOT xmlns:sql = "urn:schemas-microsoft-com:xml-sql">
<sql:header>
<sql:param name = "PKId"></sql:param>
<!-- 参数标记的外观与以前一样,但内部通过 $PKId 而不是 @PKId 引用了一个 xpath-query
标记。由于 @ 符号为 XPath 属性保留符号,因此对语法进行了必要的更改以避免混淆 -->
</sql:header>
<!-- xpath-query 标记的映射方案属性指定使用的
XDR 方案。<xpath-query> 标记内的文本包含应该应用的
XPath 查询。-->
<sql:xpath-query mapping-schema = "../schemas/detailex.xml">
/Detail[@ItemId=$PKId]
</sql:xpath-query>
</ROOT>
前面的模板引用了一个外部文件 detailex.xml。该文件为如下 XDR 方案:
<!-- 本命名域为所有 XDR 方案元素所必需 -->
<Schema xmlns="urn:schemas-microsoft-com:xml-data"
<!-- dt 命名域允许指定数据类型。但是,大部分情况下不需要。在 SQL Server 联机丛书(XML
和 Internet 支持 -> 创建使用批注 XDR 方案的 XML 视图 ->
XDR 方案批注 -> 数据类型)中有 SQL 数据类型到其方案对应类型的匹配表 -->
xmlns:dt="urn:schemas-microsoft-com:datatypes"
xmlns:sql="urn:schemas-microsoft-com:xml-sql" >
<!-- sql:relation 属性指定尽管 XML 标记称为 Detail,但是数据来自数据库中的 Items 表-->
<ElementType name="Detail" sql:relation="Items" >
<!-- <AttributeType> 标记可用于指定属性的数据类型。id 数据类型对应于数据库中的关键字。-->
<AttributeType name="ItemId" dt:type = "id"/>
<!-- 如果没有指定数据类型,则默认为字符串。-->
<AttributeType name="ItemName" />
<AttributeType name = "UnitPrice" dt:type = "r8"/>
<!-- <attribute> 标记创建一个属性。在默认情况下,假设列名与属性名相同。-->
<attribute type="ItemId" />
<attribute type="ItemName" />
<!-- 该标记上的 sql:relation 和 sql:field 属性表示结构的该部分来自其他表的数据(本例
为 RetailItems 表中的 UnitPrice 列。)-->
<attribute type="UnitPrice" sql:relation = "RetailItems"
sql:field = "UnitPrice">
<-- <sql:relationship> 标记用于指定两个表之间的连接。在本例中,它连接 Items 和
RetailItems 表。-->
<sql:relationship key-relation = "Items" key = "ItemId"
foreign-relation = "RetailItems" foreign-key = "ItemId" />
</attribute>
</ElementType>
</Schema>
方案描述 XML 输出结构和填充结构的指令,但并不限制查询—那是 XPath 查询的任务。
请考虑本 "XDR 方案" 节开头模板中的 XPath 查询。(出于演示目的,用 407 替代 $PKId。此处使用的数字为模板的一个 URL 输入。)
/Detail[@ItemId=407]
前面 XPath 查询的条件 (@ItemId = 407) 用于选择指定记录。但是,可以使用任何 XPath 查询语法允许的方式显示数据。例如,某个 XPath 查询可以选择指定范围内的记录(也就是,/Detail[@ItemId > 500 and @ItemId < 1000] )、其子项具有某种属性的记录(/Detail[@UnitPrice = 7.99]),或者任何所需条件组合。实际上,使用 XDR 方案的主要原因之一就是它们的灵活性。
请注意前面查询只引用了 XML 节点(Detail 元素和 ItemId 属性)而不是它们映射的数据库对象(Items 表和 ItemId 列)。这提供了对数据库数据的高度提取,允许在数据库发生更改时重新使用模板代码(假设方案也进行了相应的修改)。
方案可以直接通过 URL 或模板访问。下面的两个 URL 查询产生同样结果,其中一个使用模板而另一个没有使用。
在下面的 URL 中,detail.xml 引用本节开始的模板:
http://myserver/myvroot/templates/detail.xml?PKId=407&contenttype=text/html
在下面的 URL 中,detailex.xml 引用前面的 XDR 方案。XPath 查询对应的方案已添加到 URL 中:
http://myserver/myvroot/schemas/detailex.xml/Detail[@ItemId=407]
由于模板可以提供更大的灵活性,因此是一种通常采用的方法。模板中可以包含多个查询和一个可选指令,可在结果数据上应用 XSL 样式表。
下面是一个满足两种情况的 XML:
<ROOT xmlns:sql="urn:schemas-microsoft-com:xml-sql">
<Detail ItemId = "407" ItemName = ""Sure You're Joking, Mr. Feynman!"
" UnitPrice="6.99" />
</ROOT>
XDR 方案的确具有不利的限制。对于 Duwamish Online 比较严重的一点是它们不支持递归数据结构。一个表存储所有分类和销售项。第二个表在它们之间建立父/子关系。因此,要检索分类页的数据,Duwamish Online SQL XML 使用返回 XML 的存储过程。
FOR XML 子句
SQL XML 技术组的基础为数据库。我们熟悉的 SELECT 语句现在有了一个新子句:FOR XML。如名称所示,这种改动允许 SQL Server 以 XML 的字符串而不是记录集返回结果集。完全理解本节其余内容需要具有 SQL(结构化查询语言)的工作知识。
FOR XML 具有三种形式。第一种称作 FOR XML RAW,它为记录集中的每一行返回一个 <row> 标记。这种形式的应用非常有限,因为它不能有效地利用 XML 的功能描述结构化数据。第二种称作 FOR XML AUTOL,它根据表的别名和连接方式命名和组织数据。最后一种形式是 FOR XML EXPLICIT,它允许开发人员指定结果 XML 的确切结构和命名方案。FOR XML EXPLICIT 语法提供了多种帮助理顺查询和优化其执行的指示。
让我们尝试使用每一种方法来重新创建前一节中的 XML。下面的查询使用了 FOR XML RAW:
Select Detail.ItemId, Detail.ItemName, RetailItems.UnitPrice
From Items Detail
INNER JOIN RetailItems
on Detail.ItemID = RetailItems.ItemId
Where Detail.ItemId = 407 FOR XML RAW
下面是结果:
<row ItemId="407" ItemName=""Sure You're Joking, Mr. Feynman!""
UnitPrice="6.9900"/>
尽管保存了查询的所有数据,但是 row 标记不能利用 XML 的描述功能。任何使用前面数据的应用程序必须具有查询内部工作的大量高级知识。(例如,ItemId 可出现在很多表中甚至其他数据库中。在此处它表示什么?)使用同一查询,但是将 RAW 替换为 AUTO,将获得最接近预想的结果:
<Detail ItemId="407" ItemName=""Sure You're Joking, Mr. Feynman!"
"><RetailItems UnitPrice="6.9900"/></Detail>
实际结果和预想结果的唯一区别是 UnitPrice 属性放在自己的标记下,因为它来自不同的表。使用 EXPLICIT 模式,使用 XDR 方案获得的结果可以重复。查询如下所示:
Select 1 as TAG, NULL as PARENT,
Detail.ItemId as [Detail!1!ItemId],
Detail.ItemName as [Detail!1!ItemName],
RetailItems.UnitPrice as [Detail!1!UnitPrice]
From Items Detail
INNER JOIN RetailItems
on Detail.ItemID = RetailItems.ItemId
Where Detail.ItemId = 407 FOR XML EXPLICIT
该语法明显比其他两种复杂,但是它也更加强大。第一个显著区别是查询中添加了两个附加列。这两列组成了语法所需的元数据。为了便于转换,FOR XML EXPLICIT 需要每个使用的标记具有一个 ID,在此处为 1。SELECT 语句中的第一项表示该行属于 ID 为 1 的标记。后面的 NULL 选项表示没有父标记。
对 FOR XML EXPLICIT 全部功能的讨论已超出本文的范围。有关详细信息请参阅 SQL Server 联机丛书(XML 和 Internet 支持 \ 检索和编写 XML 数据 \ 检索使用 FOR XML 的 XML 文档 \ 使用显式模式)。
挑战和解决方案
数据编码:返朴归真
Duwamish Online 分类浏览用户界面设计的唯一中心目标:尽量简便直观地使用户通过尽可能少的鼠标单击次数(并且因此尽可能少的服务器访问次数 — Web 应用程序的有意义特性)导航到要去的地方。该任务由于 Duwamish Online 项分类按层次组织而得到简化。例如,大型分类诸如书籍将包含诸如小说书籍和计算机书籍等子类,而它们可能又包含子类或单独标题。我们选用当前许多电子商务站点使用的并且流行于当今 Web 的模式。当用户查找历史书籍分类列表时,在页面顶部显示导航栏(请参阅图 1)。
图 1. Duwamish Online 导航栏
这允许用户可以很方便地导航到“上”一层。要导航到“下”一层,当前显示的分类所包含的每一项均将显示一个链接。这种导航栏有多种实现方法,包括:
查看数据库并通过用户在层次中的当前位置确定路径:由于我们的数据库方案允许某一项在层次的多处显示,因此这将无法确定。例如标题“软件如何工作”将同时显示在计算机书籍和 Dewey 十进制分类下。因此,如果应用程序只知道用户在查看“软件如何工作”,将不能确定如何到达的路径。
随用户环境一起存储用户在数据库中的路径:由于这需要对后端数据库的其他访问,因此将明显减弱性能。所以,我们决定反对这种方法。
在服务器上存储用户路径每次使用任务 ID 进行访问:如果我们选择使用服务器亲合力作为 Web 基地,这是有效的。通过亲合力,在指定 Web 服务器启动任务的用户将返回到该 Web 服务器。没有亲合力,用户在访问站点过程中很可能将多次切换服务器。这意味着服务器收集的有关该用户的数据不可访问。
在 URL 中存储生成导航栏所需的所有信息:这是我们在 Duwamish Online 中选用的实现。
生成导航栏(图 1)的页面 URL 如下所示:
category.asp?PKId=829&PKName=Books&PKId=838&PKName=History
此处,明细页面收到两对参数,每对包括一个 PKId 和一个 PKName。在 ASP 中,这并不是问题,因为由这些参数指定的所有数据均可检索。但是在 SQL XML 中,将忽略重复参数(这是预料之中的,因为 SQL 不支持数组)。为了理解 Duwamish Online SQL XML 的实现,必须从实现中抽象出此处使用的数据结构。
实际上:每对 PKId 和 PKName 对应于项层次中的一个级别。导航到项层次中的较低级别将保留原始数据并新添一项。导航到层次中的较高级别将删除一个或多个上一次添加到数据结构中的项。后入先出 (LIFO) 数据范例是堆栈的特征。Duwamish Online 的实现在许多方面模拟堆栈的数组实现,因为它允许随机访问。
由于 Duwamish Online 中的重复参数方案不能在 Duwamish Online SQL XML 中工作,因此为 SQL XML 创建数组方式实现的唯一方法是使用 PKId 和 PKName 分隔列表。不幸的是,这导致了两个主要问题。第一个问题是找到合适的分隔符。更为严重的第二个问题是 SQL 不能进行字符串处理,任何字符串处理例称明显是事后处理。尝试数组方式实现将在数据库侧导致明显不必要的负担和一些费解的代码。
重要的是注意到只有堆栈顶端的项用于从数据库检索数据;其他项专门用于构建导航栏。因此,我决定创建类似于链接列表的实现。传入模板的三个参数为 PKId、PKName 和 Path。PKId 和 PKName 包含要检索的当前项的信息,使数据库服务器快速有效地返回相应的信息。Path 包含生成导航栏所需的所有信息。现在我们来看看它如何工作。
当用户查看顶级分类时,Path 为空而 URL 请求如下所示:
PKId=829&PKName=Books
用户选择某个子类(例如,历史)后,前一请求赋给 Path 变量。但是,为了避免 & 符号混淆解析器,请求首先进行 URL 传输编码。经过这一步后,查询如下所示:
PKId%3D829%26PKName%3Dbooks
该字符串赋给变量 Path,并与相应的用户选项 PKId 和 PKName 相结合创建新的请求。请求如下所示:
PKId=838&PKName=History&Path=PKId%3D829%26PKName%3Dbooks
要创建新项的请求,将前面的请求作为字符串并获得 URL 传输编码:
PKId%3D838%26PKName%3DHistory%26Path%3DPKId%253D829%2526PKName%253Dbooks
这将成为层次中下一级的 Path 变量。为了生成导航栏,使用了反转过程。对于如前所示的字符串,算法首先解码字符串。下一步,解析出 PKId 和 PKName 生成导航栏项。Path 变量则包含下一步操作的编码字符串。字符串进行解码并解析给 PKId、PKName 和 Path 节。其他导航栏项通过 PKId 和 PKName 生成,并重复该过程直到所有数据解析完毕。
下面是该算法如何集成到样式表中的框架概要:
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/TR/WD-xsl">
<!-- 这是 XSL 样式表的一种标准。不幸的是,XSLT 不允许脚本,因此不能在此处使用。-->
<xsl:template match = "/">
<!-- info 节点包含元数据。所包含的数据直接从 URL 参数中提取,在数据库服务器上未进行
任何修改。-->
<xsl:for-each select = "ROOT/Info">
<!-- 脚本通过 <xsl:eval> 标记调用。函数调用的返回值插入到结果文档中。此处,函数并不
返回任何值。传递给函数的 "this" 引用指向当前环境的 XMLDOM 节点。
-->
<xsl:eval>parsePath(this)</xsl:eval>
<!-- parsePath 函数在脚本中以易于检索的形式创建存储 Path 参数信息的数据结构。为了在
数据结构中迭代,样式表然后调用 Info
模板(请参阅下面内容)。-->
<xsl:apply-templates select = "." />
</xsl:for-each>
</xsl:template>
<xsl:template match = "Info">
<!-- XSL 模板使用脚本条件判断是否到达实际结果的末尾,如果条件为真,则重新执行。这与
"while" 循环的 XSL 版本类似。-->
<xsl:if expr = "nextNode()">
<!-- 在结果文档中插入当前项的 HTML 链接(为了清晰起见而忽略了实际代码)。-->
<!-- 重新执行模板。-->
<xsl:apply-templates select = "." />
</xsl:if>
</xsl:template>
<xsl:script>
<!-- 有必要将脚本封装在 CDATA 块中以避免混淆 XML 解析器。-->
<![CDATA[
// PathItems 是对保存 parsePath 分析结果的数据结构的引用
var PathItems;
// PathItemsIndex 包含数据结构的当前位置
var PathItemsIndex;
// parsePath 函数解析 XML 中包含的元数据并将其放入脚本中的数据结构
function parsePath(node) {
// 为了清晰起见忽略了实际节点
}
function nextNode() {
// PathItemsIndex 变量递减而表示递增
PathItemsIndex--;
// 如果路径中包含多个项,函数返回真,否则返回假。 items in the path,
return (PathItemsIndex >= 0);
}
]]>
</xsl:script>
</xsl:stylesheet>
由于该算法在服务器上的转换时间运行,因此客户端可保持无脚本。
字符串失去控制
Duwamish Online 在中间层组件进行大量字符串处理。字符串处理在利用多种技术的应用程序中非常必要,因为在某一环境中安全的字符串在另一环境中可能导致严重问题。例如 "a < b" 在数据库中存储没有问题,但是需要避免 "a < b" 以便在 Web 页上正确显示。为了能作为 URL 的参数正确传输,仍然需要避免这种情况,因为 & 在此环境中表示完全不同的含义。
如前所示,由于 SQL 缺乏有效执行所需的字符串处理功能,因此我将字符串处理例程置于 XSL 样式表中的脚本块内部。请注意脚本在 Web 服务器上转换时间运行,使应用程序可返回能够正确显示的 HTML,与客户端的浏览器功能无关。
性能
在最近的测试中,我们使用了 Web 应用程序测试工具(http://webtool.rte.microsoft.com/home.htm(英文))比较 Duwamish Online 和 Duwamish Online SQL XML 的分类浏览性能。我们在安装了应用程序所有组件的 Dell Pentium III 550Mz Xeon Dual-Proc(256 MB RAM)计算机上进行了测试。结果如图 2 所示。
图 2. 性能比较测试
如您所见,Duwamish Online SQL XML 具有切实的性能提高。实际上,这种提高甚至比预想的还好,因为 Duwamish Online 利用了缓存(因此指定分类或明细项只从数据库检索一次)。 Duwamish Online 的有缓存性能测试比无缓存性能测试有十成的提高。
Microsoft SQL XML 技术预览(http://msdn.microsoft.com/xml/articles/xmlsql/sqlxml_prev.asp(英文))将 ISAPI 缓存作为功能集的一部分。在前面对该缓存功能的测试中,其性能与无缓存的 SQL XML 版本相比有巨大提高。不幸的是,SQL Server 2000 的发布版本不包括缓存功能。但是,缓存功能将包括在 Web 发布服务包中。
结论
SQL XML 非常适于创建数据驱动页。通常,最终结果与 ASP 应用程序结果相同,但是代码更少、性能更好、维护量更少。在数据需要大量处理后再返回客户端或复杂显示逻辑的情况下,其优点可能稍有减弱,但是 SQL XML 是一种不应忽视的新型 Web 技术。