 | 级别: 中级 Vikram Vaswani, 创始人, Melonfire
2007 年 9 月 20 日 您是否想过用一种简单的方法将 SQL 结果集转换为 XML?PEAR 包 XML_Query2XML 提供的一种全面性框架可以有效地将数据库查询结果转换为可定制的 XML 文档。本文将介绍这个包,并演示有用的实际应用程序,包括将它与 XSL 和 XPath 结合使用,并与来自外部 Web 服务的数据相结合,创建数据库转储文件。
简介
也许您曾经听过 PEAR,PHP 扩展和应用库(PHP Extension and Application
Repository)。这个社区推动的项目的目标是,提供一个广泛的、高质量代码的开源库,协助 PHP 开发人员快速开发应用程序。与 Perl 的 CPAN 存储库的概念类似,PEAR 一直以来都是我首先关注的有趣、有用的 PHP+XML 小部件。这些部件包括:XML_Serializer 类,用于方便地将 PHP 数据结构序列化为 XML 对象;XML_XUL 类,为构造 Mozilla XUL 应用程序提供了一个 API;XML_SVG 类,为通过编程构造 SVG 格式的向量图提供方法;等等。
在本文中,我还将向您介绍 PEAR 的 XML
部分的另一个成员,XML_Query2XML 类。此类提供了一个 API,用于快速有效地将 SQL 结果集转换为格式良好的 XML。如果稍加创新,通过 XSL 转换可以很轻松地将此输出转换为其他格式,或与其他基于 XML 的应用程序集成。
安装所需的软件
XML_Query2XML 包由 Lukas Feiler 积极地开发和维护,并在 LGPL 许可下发布给 PHP 社区。它需要使用 PHP 5.0(或更高版本)运行。安装它的最简单的方法是使用 PEAR 自动安装程序,此程序应该默认包含在 PHP 构建中。要安装此包,只需在 shell 提示中发出以下命令即可:
shell>pear install XML_Query2XML
PEAR 安装程序将连接到 PEAR 包服务器,下载此包并将其安装到系统中合适的位置。
要手动安装此包,请访问它在 PEAR Web 站点上的主页,下载包归档文件并手动地将这些文件解压缩到所需位置。注意,手动安装过程要求用户对 PEAR 的包组织结构有一些了解。
现在,您还应该意识到一些其他依赖项:
- XML_Query2XML 使用 DB、MDB2 或 ADOdb 数据库的抽象层与目标 RDBMS 通信,并因此要求存在这样一个抽象层,而且进行了正确安装,还要求有一个合适的数据库驱动程序。本文所给出的例子使用了 MDB2
抽象层(PEAR 包树的一部分)和它的 MySQL driver MDB2_Driver_mysql。正如前面所提到的,您可以使用 PEAR 自动安装程序安装这两个包,另外,您也可以从 PEAR Web 站点下载它们。
- 本文中的例子使用 MySQL 的示例数据库
world,其中包含了各种预填充的链接表,表中存储了城市数据和国家数据。有关获取和设置 world 数据库的指导,可以参考本文的 参考资料 部分。
- 本文中的例子要求 PHP 构建能够支持 PHP 的 DOM、XSL 和 SimpleXML 函数。PHP 5.x 中默认支持这些函数。
- 要求具有使用 PHP 的 DOM 和 SimpleXML 函数以及 XML、XPath、XSL 技术的实际经验。
有关这些不同组件的信息及下载链接,请参阅 参考资料。
本文中的所有例子都已使用 XML_Query2XML Version 1.2.1 进行了测试。
将 SQL 转换为 XML
成功安装完所有需要的组件后,就可以使用下面的示例 PHP 脚本开始探索 XML_Query2XML:
清单 1. 示例 SQL-to-XML 转换
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initialize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql = "SELECT * FROM Country";
$xml = $q2x->getFlatXML($sql);
// send output to browser
header('Content-Type: text/xml');
$xml->formatOutput = true;
echo $xml->saveXML();
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
这段脚本演示了 XML_Query2XML 类的基本用法。首先,脚本包含了 XML_Query2XML 和 MDB2 类文件,然后通过它的 factory() 方法初始化了一个 MDB2 抽象层实例。此方法接受 DSN 作为输入,其中包含了关于 RDBMS 类型、RDBMS 用户名和密码,以及目标数据库名称的信息。得到的 MDB2 实例然后用于初始化 XML_Query2XML 实例,后者由 $q2x 对象表示。
构造 DSN 并创建 XML_Query2XML 对象实例后,即可实际对 RDBMS 执行 SQL 查询并将结果转换为 XML。通过 XML_Query2XML 的 getFlatXML() 方法可以完成这一任务,而该方法经常用于简单的 SELECT 类型的查询。此方法的输出是格式良好的 XML 文档,其中对 SQL 结果集进行了编码。生成的 XML 结果类似于:
清单 2. 由清单 1 生成的 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<code>AFG</code>
<name>Afghanistan</name>
<continent>Asia</continent>
<region>Southern and Central Asia</region>
<surfacearea>652090.00</surfacearea>
<indepyear>1919</indepyear>
<population>22720000</population>
<lifeexpectancy>45.9</lifeexpectancy>
<gnp>5976.00</gnp>
<gnpold></gnpold>
<localname>Afganistan/Afqanestan</localname>
<governmentform>Islamic Emirate</governmentform>
<headofstate>Mohammad Omar</headofstate>
<capital>1</capital>
<code2>AF</code2>
</row>
<row>
<code>NLD</code>
<name>Netherlands</name>
<continent>Europe</continent>
<region>Western Europe</region>
<surfacearea>41526.00</surfacearea>
<indepyear>1581</indepyear>
<population>15864000</population>
<lifeexpectancy>78.3</lifeexpectancy>
<gnp>371362.00</gnp>
<gnpold>360478.00</gnpold>
<localname>Nederland</localname>
<governmentform>Constitutional Monarchy</governmentform>
<headofstate>Beatrix</headofstate>
<capital>5</capital>
<code2>NL</code2>
</row>
<row>
<code>ANT</code>
<name>Netherlands Antilles</name>
<continent>North America</continent>
<region>Caribbean</region>
<surfacearea>800.00</surfacearea>
<indepyear></indepyear>
<population>217000</population>
<lifeexpectancy>74.7</lifeexpectancy>
<gnp>1941.00</gnp>
<gnpold></gnpold>
<localname>Nederlandse Antillen</localname>
<governmentform>Nonmetropolitan Territory of
The Netherlands</governmentform>
<headofstate>Beatrix</headofstate>
<capital>33</capital>
<code2>AN</code2>
</row>
...
</root>
|
仔细查看一下上面的 XML 输出可以看到一个清晰的结构。来自 SQL 结果集的每条记录都表示为一个 <row> 元素,而每条记录的单个字段则嵌入到每个相应的 <row> 中。被嵌入元素的名称与被查询表中的字段名称对应,而文档元素 — XML 树的根 — 被相应地命名为 <root>。
使用 XSL 转换 XML 输出
当然,从 SQL 查询生成 XML 通常只是完成了一半任务;剩下的一半任务是使用生成的 XML。使用 XML 文档可以做很多工作,但是其中最重要的一项就是使用 XSL Transformation 将其转换为一些其他格式,比如 HTML 或 RSS。明白这一点后,我们来快速制作一个 XSL 样式表,将 清单 2 的 XML 输出转换为一个简单的 HTML 页面。
清单 3. 一个 XSL 样式表
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/root">
<html>
<head>
<style type="text/css">
td { text-align: center; padding: 3px; }
.head { font-style: italic; }
</style>
</head>
<body>
<table border="1">
<thead>
<tr>
<xsl:for-each select="row[1]/*">
<td class="head">
<xsl:value-of select="local-name(.)"/>
</td>
</xsl:for-each>
</tr>
</thead>
<tbody>
<xsl:apply-templates/>
</tbody>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="row">
<tr>
<xsl:apply-templates/>
</tr>
</xsl:template>
<xsl:template match="row/*">
<td>
<xsl:value-of select="."/>
</td>
</xsl:template>
</xsl:stylesheet>
|
在清单 4 中您会看见修改后的 PHP 脚本,它现在使用 PHP 的 XSL 函数转换 XML_Query2XML 生成的输出:
清单 4. 使用 XSL 转换 SQL-to-XML 输出
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initalize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql = "SELECT * FROM Country";
$xml = $q2x->getFlatXML($sql);
// read XSL stylesheet data
$xsl = new DOMDocument;
$xsl->load('country.xsl');
// initialize XSLT engine
$xslp = new XSLTProcessor;
// attach XSL stylesheet object
$xslp->importStyleSheet($xsl);
// perform transformation
header('Content-Type: text/html');
echo $xslp->transformToXML($xml);
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
这段脚本的第一部分与 清单 1 类似;它所生成的 XML 文档包含了 SQL 查询的结果并将其作为 DOMDocument 实例存储在 $xml 中。接下来,初始化 XSLTProcessor 类的一个实例,并使用类的 importStyleSheet() 方法导入 XSL 的样式表。transformToXML() 方法接受源 XML 数据作为输入参数,然后使用 XSL 样式表中指定的规则将 XML 文档转换为 HTML 页面。
图 1 给出了输出结果:
图 1. 由清单 4 生成的 HTML 文档
定制 XML 输出
前面示例中所展示的 getFlatXML() 方法在只需要快速实现 SQL-to-XML 转换时很有用。但是,如果您需要执行某些更复杂的操作时 — 例如,将某些结果集字段作为属性而不是元素显示,或定义自己的元素名称 — 您应该使用 XML_Query2XML 中的 getXML() 方法。此方法让您能够广泛地定制输出 XML,包括其结构和样式。
清单 5 中给出了一个例子:
清单 5. 定制 SQL-to-XML 输出
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initalize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql = "SELECT * FROM Country";
$xml = $q2x->getXML($sql, array(
'idColumn' => 'code',
'rootTag' => 'countries',
'rowTag' => 'country',
'attributes' => array('code'),
'elements' => array('name', 'continent', 'area' => 'surfacearea')
)
);
// send output to browser
header('Content-Type: text/xml');
$xml->formatOutput = true;
echo $xml->saveXML();
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
getXML() 方法接受两个参数:待执行的 SQL 查询,和定义 XML 输出格式的一组选项。表 1 说明了前一个清单中每个选项的含义:
表 1. getXML() 方法的选项
| 选项 | 控制内容 |
|---|
| rootTag | 文档元素的名称(默认:root) |
|---|
| rowTag | 表示每个结果行的元素的名称(默认:row) |
|---|
| idColumn | 结果集的主键字段 |
|---|
| attributes | 一个显示为 XML 属性的字段列表 |
|---|
| elements | 一个显示为 XML 元素的字段列表 |
|---|
清单 6 给出了清单 5 的脚本的输出:
清单 6. 由清单 5 生成的 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
<country code="AFG">
<name>Afghanistan</name>
<continent>Asia</continent>
<area>652090.00</area>
</country>
<country code="NLD">
<name>Netherlands</name>
<continent>Europe</continent>
<area>41526.00</area>
</country>
<country code="ANT">
<name>Netherlands Antilles</name>
<continent>North America</continent>
<area>800.00</area>
</country>
...
</countries>
|
注意,这个 XML 文档没有包含来自 SQL 结果集的所有字段,而是只包含了 elements 和 attributes 数组中所指定的那些字段,而且 attributes 数组中指定的字段显示为每个 <country> 元素的属性,而不是子节点。
您还会记得,输出 XML 中的元素和属性名称默认为相应的字段名称。但是,当使用 getXML() 方法时,您可以通过在 attributes 和 elements 数组中指定替代值作为键-值对,修改这些默认名称。例如:SQL 结果集中的 surfacearea 字段在 XML 输出中简单地显示为 <area> 元素。
有关如何定制 getXML() 方法的输出的示例,请查看 XML_Query2XML 手册(参阅 参考资料)。
使用 SQL 连接
XML_Query2XML 还提供了一个框架,利用该框架可以使用 XML 将一个结果集的内容嵌入到另一个结果集中。此特性在处理连接或以某种方式链接的查询最为常用;如果您出于性能考虑,需要将一个大查询分解为多个小查询,使用该框架也很方便。
为了更好地理解这一点,我们回到 world 数据库并考虑它的两个表 Country 和 City,此二者通过 code 外键相互链接在一起。
现在,假设您希望生成一个 XML 文档树,它将多个 <city> 元素嵌入到外部的 <country> 元素中。我们再假设您希望将输出限制为每个国家的人口最多的五个城市,并且您希望字段值表示为属性而不是元素。简言之,假设您需要一个如下所示的 XML 文档:
清单 7. SQL 连接之后的预期 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
<country code="IND" name="India" region="Southern and Central Asia">
<cities>
<city name="Mumbai (Bombay)" district="Maharashtra" population="10500000"/>
<city .../>
<city .../>
<city .../>
<city .../>
</cities>
</country>
<country ...>
...
</country>
...
</countries>
|
清单 8 中的代码用于生成这样的嵌套 XML 文档:
清单 8. 从 SQL 连接创建定制的 XML 输出
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initalize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql_1 = "SELECT * FROM Country";
$sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
$xml = $q2x->getXML($sql_1, array(
'idColumn' => 'code',
'rootTag' => 'countries',
'rowTag' => 'country',
'attributes' => array('code', 'name', 'continent'),
'elements' => array('cities' => array(
'sql' => array('data' => array('code'), 'query' => $sql_2),
'idColumn' => 'id',
'rootTag' => 'cities',
'rowTag' => 'city',
'attributes' => array('name','district','population'))
)
)
);
// send output to browser
header('Content-Type: text/xml');
$xml->formatOutput = true;
echo $xml->saveXML();
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
此处要注意的关键是 elements
数组。清单 5 中该数组只包含一个显示为元素的结果集字段列表,而在这里,该数组执行了一个复杂得多的函数。它首先定义了一个新元素 <cities>,然后将它链接到一个包含名称-值对的选项数组。此选项数组中的惟一新键为 sql 键,它定义了运行内部 SQL 查询以填充 <cities> 元素。
有必要花点时间了解一下这个 sql 键。此键被链接到一个关联数组,数组本身包含了两个键:
-
data,指定从外部 SQL 查询导入的字段
-
query,指定填充 <cities> 元素时要运行的内部 SQL 查询
注意,第二个 SQL 查询包含了一个问号(?)占位符 — 运行时,此占位符被 data 数组中指定的字段当前值替代。或者,使用一个实际的例子,如果外部查询返回的记录包含 code 字段的 'IND' 值,则这个 'IND' 值随后将被内插到内部查询中,替代 ? 占位符。
XML_Query2XML 的神奇之处现在应该清楚了。您可以使用单独的 SQL 查询填充每个 elements 数组,从而允许 SQL 结果集被嵌入到无限制的深度。此外,因为每个 elements 数组可以引用父查询的字段,所以可以创建一系列链式查询(与 SQL 连接相似),通过特定的命名字段相互链接。
输出结果如下:
清单 9. 由清单 8 生成的 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
<country code="AFG" name="Afghanistan" continent="Asia">
<cities>
<city name="Kabul" district="Kabol" population="1780000"/>
<city name="Qandahar" district="Qandahar" population="237500"/>
<city name="Herat" district="Herat" population="186800"/>
<city name="Mazar-e-Sharif" district="Balkh" population="127800"/>
</cities>
</country>
<country code="NLD" name="Netherlands" continent="Europe">
<cities>
<city name="Amsterdam" district="Noord-Holland" population="731200"/>
<city name="Rotterdam" district="Zuid-Holland" population="593321"/>
<city name="Haag" district="Zuid-Holland" population="440900"/>
<city name="Utrecht" district="Utrecht" population="234323"/>
<city name="Eindhoven" district="Noord-Brabant" population="201843"/>
</cities>
</country>
...
</countries>
|
走到这一步,生成新的 XSL 样式表说明新 XML 结构就很容易了:
清单 10. 转换清单 9 的 XSL 样式表
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/countries">
<html>
<head>
<style type="text/css">
td { text-align: center; padding: 3px; }
.head { font-style: italic; }
</style>
</head>
<body>
<xsl:for-each select="country">
<h2><xsl:value-of select="@name"/> - <xsl:value-of
select="@continent"/></h2>
<table border="1">
<thead>
<tr>
<xsl:for-each select="cities/city[1]/@*">
<td class="head">
<xsl:value-of select="name(.)"/>
</td>
</xsl:for-each>
</tr>
</thead>
<tbody>
<xsl:apply-templates/>
</tbody>
</table>
</xsl:for-each>
</body>
</html>
</xsl:template>
<xsl:template match="cities/city">
<tr>
<xsl:for-each select="@*">
<td>
<xsl:value-of select="."/>
</td>
</xsl:for-each>
</tr>
</xsl:template>
</xsl:stylesheet>
|
当然,您接下来也可以修改原始的 PHP 脚本以使用此样式表转换生成的 XML。但是,改动很小:
清单 11. 转换清单 8 生成的 XML 输出
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initalize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql_1 = "SELECT * FROM Country";
$sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
$xml = $q2x->getXML($sql_1, array(
'idColumn' => 'code',
'rootTag' => 'countries',
'rowTag' => 'country',
'attributes' => array('code', 'name', 'continent'),
'elements' => array('cities' => array(
'sql' => array('data' => array('code'), 'query' => $sql_2),
'idColumn' => 'id',
'rootTag' => 'cities',
'rowTag' => 'city',
'attributes' => array('name','district','population'))
)
)
);
// read XSL stylesheet data
$xsl = new DOMDocument;
$xsl->load('countries.xsl');
// initialize XSLT engine
$xslp = new XSLTProcessor;
// attach XSL stylesheet object
$xslp->importStyleSheet($xsl);
// perform transformation
header('Content-Type: text/html');
echo $xslp->transformToXML($xml);
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
图 2 显示了转换后的 XML 的外观:
图 2. 由清单 11 生成的 HTML 文档
您可以以多种方式使用这种嵌套功能,并且 XML_Query2XML 也提供了各种选项以进一步调整 XML 输出。您可以在 XML_Query2XML 手册中找到详细示例(参阅 参考资料)。
使用 XPath 过滤 SQL 记录
您可以想像,约束 getXML() 方法的输出,使其匹配某些约束条件非常容易。只需将合适的 WHERE 子句添加到 SQL 查询中即可。另外也可以使用 XPath 构造创建 XML 节点树的过滤子集并将其返回给调用方。
在清单 12 中,查看一个实现此功能的简单示例,修改 清单 11 并约束输出 XML 以便只列出欧洲的国家和城市,使用了一个 XPath 条件:
清单 12. 使用 XPath 约束 SQL-to-XML 输出
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initialize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql_1 = "SELECT * FROM Country";
$sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
$xml = $q2x->getXML($sql_1, array(
'idColumn' => 'code',
'rootTag' => 'countries',
'rowTag' => 'country',
'attributes' => array('code', 'name', 'continent'),
'elements' => array('cities' => array(
'sql' => array('data' => array('code'), 'query' => $sql_2),
'idColumn' => 'id',
'rootTag' => 'cities',
'rowTag' => 'city',
'attributes' => array('name','district','population'))
)
)
);
// now, further filter the XML using XPath
// return only those <country> nodes which have the attribute 'continent=Europe'
// as a DOMNodeList
$xpath = new DOMXPath($xml);
$nodelist = $xpath->query("/countries/country[@continent='Europe']");
// generate a new DOM tree using the XPath result set
// create the root element
// import each node from the node list and append to the new DOM tree
$dom = new DOMDocument;
$root = $dom->createElement('countries');
$dom->appendChild($root);
$x = 0;
while ($node = $nodelist->item($x)) {
$node = $dom->importNode($node, true);
$root->appendChild($node);
$x++;
}
// print XML
header('Content-Type: text/xml');
$dom->formatOutput = true;
echo $dom->saveXML();
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
这段脚本的第一小段跟以前一样 — 两个嵌套的 SQL 查询,内部查询使用外部查询的数据生成一个国家和城市数据列表。但是此时,并没有立即打印 XML 或将其传递给 XSLT 处理器,而是初始化了 DOMXPath 对象,并从原始的 XML 树创建了一个新的 DOMNodeList。此 DOMNodeList 使用 XPath 查询以确保它只包含 continent 属性的值为 Europe 的 <country> 元素。一旦创建 DOMNodeList 之后,就会初始化新的 DOMDocument 并将此 DOMNodeList 一个节点接一个节点地导入其中,生成一个新的 XML 文档。
清单 13 给出了输出的一个片段:
清单 13. 由清单 12 生成的 XML 输出(经过了简化)
<?xml version="1.0"?>
<countries>
<country code="NLD" name="Netherlands" continent="Europe">
<cities>
<city name="Amsterdam" district="Noord-Holland" population="731200"/>
<city name="Rotterdam" district="Zuid-Holland" population="593321"/>
<city name="Haag" district="Zuid-Holland" population="440900"/>
<city name="Utrecht" district="Utrecht" population="234323"/>
<city name="Eindhoven" district="Noord-Brabant" population="201843"/>
</cities>
</country>
<country code="ALB" name="Albania" continent="Europe">
<cities>
<city name="Tirana" district="Tirana" population="270000"/>
</cities>
</country>
<country code="AND" name="Andorra" continent="Europe">
<cities>
<city name="Andorra la Vella" district="Andorra la Vella" population="21189"/>
</cities>
</country>
<country code="BEL" name="Belgium" continent="Europe">
<cities>
<city name="Antwerpen" district="Antwerpen" population="446525"/>
<city name="Gent" district="East Flanderi" population="224180"/>
<city name="Charleroi" district="Hainaut" population="200827"/>
<city name="Liège" district="Liège" population="185639"/>
<city name="Bruxelles [Brussel]" district="Bryssel" population="133859"/>
</cities>
</country>
...
<countries>
|

 |

|
合并来自多个源的数据
在基于 XML 的应用程序的实际开发中,XML 文档中保存的信息不太可能来自一个源。除了一个或多个 SQL 结果集外,它还可能包含来自磁盘文件的数据、来自外部 Web 服务的数据,以及来自系统进程表的数据。为了应对这些情形,XML_Query2XML 提供了一种方法,用于将来自非 SQL 源的数据集成到 getXML() 方法返回的 XML 中。
XML_Query2XML 允许开发人员定义定制的回调函数,由输出 XML 中的特定元素调用。这些回调函数将在内部获得所需的数据,将其转换为 XML,并将此 XML(像 DOMNode 实例一样)返回给调用方,适合于在 XML 文档树中合适的位置插入。这些回调函数必须跟在 getXML() 调用中的散列(#)符号之后,并将自动接收当前 SQL 记录作为输入。
您也许会问这是否真的有用。这个问题最好用示例回答。首先,假设您希望生成一个 XML 文档,其中列出一些国家及其人口最多的城市。您前面见过的许多示例都可以实现这个功能。为了让示例变得更有趣些,我们对此 XML 进行增强,使用 GeoNames Web 服务的数据给 XML 附上每个指定城市的经纬度。
清单 14 给出了代码:
清单 14. 将 Web 服务数据与 SQL-to-XML 输出集成
<?php
ini_set('max_execution_time', 120);
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
try {
// initalize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
// generate SQL query
// get results as XML
$sql = "SELECT Country.Code2 AS code, Country.Name AS country, City.Name AS city,
City.Population AS population FROM Country, City
WHERE Country.Code = City.CountryCode GROUP BY City.CountryCode
HAVING City.Population = MAX(City.Population) ORDER BY City.Population
DESC LIMIT 15";
$xml = $q2x->getXML($sql, array(
'idColumn' => 'code',
'rootTag' => 'countries',
'rowTag' => 'country',
'attributes' => array('code', 'name' => 'country'),
'elements' => array('city' => array (
'elements' => array(
'name' => 'city',
'population',
'location' => '#getLocation'),
)
),
)
);
// print XML
header('Content-Type: text/html');
$xml->formatOutput = true;
print $xml->saveXML();
} catch (Exception $e) {
echo $e->getMessage();
}
// function to get data from GeoNames Web service
// call GeoNames with country code and city name
// create XML document fragment with returned values
function getLocation($record) {
// get data and format into SimpleXML object
$sxml = simplexml_load_string(file_get_contents(
"http://ws.geonames.org/search?maxRows=1&name=" .
urlencode(utf8_encode($record['city'])) . "&country=" .
urlencode(utf8_encode($record['code']))));
// extract data from SimpleXML object
// convert into DOMNode fragments
$dom = new DOMDocument();
// generate <lat> node
$lat = $dom->createElement('lat');
$lat->appendChild($dom->createTextNode($sxml->geoname{0}->lat));
// generate <long> node
$long = $dom->createElement('long');
$long->appendChild($dom->createTextNode($sxml->geoname{0}->lng));
return array($lat, $long);
}
?>
|
在 清单 14 中,对 getXML() 的调用执行 SELECT 查询,该查询将各个城市按其国家代码分组,然后选择一个人口数量最多的城市。此数据然后被转换为下面的 XML 文档(清单 15):
清单 15. 由清单 14 生成的第一阶段 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
<country code="AW" name="Aruba">
<city>
<name>Oranjestad</name>
<population>29034</population>
</city>
</country>
...
</countries>
|
下一个任务是获取每个城市的经纬度并将其插入到上面的文档树中(参见 清单 14)。此信息来自 GeoNames Web 服务,可以通过 REST 访问此服务,并公开一个 search() 方法,用于返回指定位置的地理信息。对 Web 服务进行完整描述超出了本文的范围,但是您可以在本文参考资料部分阅读更多信息。
清单 16 显示了一个查询 'Berlin, Germany' 时的 GeoNames 响应包示例:
清单 16. 一个 GeoNames 响应包示例
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<geonames>
<totalResultsCount>807</totalResultsCount>
<geoname>
<name>Berlin</name>
<lat>52.5166667</lat>
<lng>13.4</lng>
<geonameId>2950159</geonameId>
<countryCode>DE</countryCode>
<countryName>Germany</countryName>
<fcl>P</fcl>
<fcode>PPLC</fcode>
</geoname>
</geonames>
|
正如您所看到的,这个响应包包含了关于指定位置的各种信息,包括我们最为关心的:经纬度。
现在,仔细查看 清单 13 中的 getXML() 调用。注意,选项数组的 location 键并没有链接到查询结果集中的字段,而是链接到了回调函数 getLocation()。这意味着每次 getXML() 处理来自 SQL 结果集的记录时,它将同一条记录作为字段-值对的关联数组传递给 getLocation() 回调函数。getLocation() 方法反过来使用 REST 调用 GeoNames Web 服务的 search() 方法,将来自 SQL 记录的城市和国家名称作为参数传递给它,并作为 SimpleXML 对象搜索响应。SimpleXML 表示法然后可用于在响应包中寻找 <lat> 和 <lng> 元素,将它们转换为两个单独的 DOMNode 实例,并将它们传回 getXML() 作为数组插入树中。
最后,清单 17 给出了输出 XML 的外观:
清单 17. 由清单 14 生成的最终 XML 输出(经过了简化)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
<country code="IN" name="India">
<city>
<name>Mumbai (Bombay)</name>
<population>10500000</population>
<location>
<lat>18.975</lat>
<long>72.8258333</long>
</location>
</city>
</country>
<country code="KR" name="South Korea">
<city>
<name>Seoul</name>
<population>9981619</population>
<location>
<lat>37.5663889</lat>
<long>126.9997222</long>
</location>
</city>
</country>
...
</countries>
|
正如此示例所阐明那样,使用定制的回调函数是一种将其他源的数据提取到
getXML() 生成的 XML 输出中的简单方法。清单 14 连接到外部 Web 服务;您可以同样轻松地将外部文件或 XML-RPC 调用的输出导入到最终的 XML 树中。
创建数据库备份
XML_Query2XML 的另外一个有用的应用是将数据库表的内容转储为基于 XML 的格式,用于存储和备份目的。这类备份脚本背后的逻辑很简单:从数据库取得一个表列表,迭代此列表并使用如 DESC ? 和 SELECT * FROM ? SQL 之类的命令分别提取每个表的模式和记录。如果您跟随本文的思路,可能已经想到需要使用 getXML() 方法调用以完成此任务。
执行此任务比最初预想的要困难一些,主要是由于 MDB2 抽象层有一些限制:即,在准备好的查询中不能处理列和表名的占位符。这让使用之前提到的 DESC
? 和 SELECT * FROM ? 查询非常困难,因为 MDB2 层在遇到此类查询时将只是生成错误。
那该怎么办?发挥一点创造力,如清单 18 所示:
清单 18. 生成数据库结构和内容的 XML 清单
<?php
ini_set('max_execution_time', 120);
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';
// set database name
$db = 'world';
try {
// initialize Query2XML object
$q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/' . $db));
// SQL query to get table list
// note: this SQL query varies from database to database
$sql = "SHOW TABLES";
$xml = $q2x->getXML($sql, array(
'idColumn' => false,
'rootTag' => 'database',
'rowTag' => 'table',
'attributes' => array('name' => 'tables_in_' . $db))
);
// get a list of all the <table> nodes
$nodelist = $xml->getElementsByTagName("table");
// iterate over the nodes
$x = 0;
while ($node = $nodelist->item($x)) {
// extract the table name
$table = $node->attributes->getNamedItem('name')->nodeValue;
// get table description
// as DOM document
// note: this SQL query varies from database to database
$sql_1 = 'DESC ' . $table;
$schema = $q2x->getXML($sql_1, array (
'idColumn' => 'field',
'rowTag' => 'define',
'rootTag' => 'schema',
'elements' => array('*'))
);
// get table contents
// as another DOM document
$sql_2 = 'SELECT * FROM ' . $table;
$data = $q2x->getXML($sql_2, array (
'idColumn' => false,
'rowTag' => 'record',
'rootTag' => 'data',
'elements' => array('*'))
);
// iterate over the $schema DOM document
// use XPath to get the <schema> node and all its children
// import it into the main XML tree, under the corresponding <table> element
// credit: Igor Kraus, http://www.php.net/simplexml for this suggestion
$xpath = new DOMXPath($schema);
$query = $xpath->query('//schema');
for ($i = 0; $i < $query->length; $i++) {
$xml->documentElement->childNodes->item($x)->appendChild(
$xml->importNode($query->item($i), true));
}
// do the same for the $data DOM document
$xpath = new DOMXPath($data);
$query = $xpath->query('//data');
for ($i = 0; $i < $query->length; $i++) {
$xml->documentElement->childNodes->item($x)->appendChild(
$xml->importNode($query->item($i), true));
}
// increment counter for the next run
$x++;
}
// write output to disk
// print success/error message
$xml->formatOutput = true;
if ($xml->save('/tmp/dump.xml')) {
echo 'Data successfully saved!';
} else {
echo 'Data could not be saved!';
}
} catch (Exception $e) {
echo $e->getMessage();
}
?>
|
这看起来很复杂,但实际上很简单:
-
首先,获取当前数据库中所有表的名称。获取此列表的 SQL
命令随数据库的不同而有所不同,清单 18 中的脚本使用 MySQL 的
SHOW
TABLES 命令,但是这并不能在不同的 RDBMS 之间移植。如果使用不同的数据库系统,您可能需要更改命令。此命令的输出是 XML 文档,存储为 $xml 并且类似于清单 19:
清单 19. 由清单 18 生成的第一阶段 XML 输出
<?xml version="1.0" encoding="UTF-8"?>
<database>
<table name="City"/>
<table name="Country"/>
<table name="CountryLanguage"/>
</database>
|
- 下一步,使用
getElementsByTagName() 方法获取 前一步 中生成的 <table> 元素的集合,后面在一个循环中对其进行了处理,在循环的每次迭代中,创建了两个新的 XML 文档:$schema 包含了关于表的字段结构的信息(见 清单 20),$data 保存了表的实际记录(参见 清单 21):
清单 20. 包含表模式的 XML 文档
<?xml version="1.0" encoding="UTF-8"?>
<schema>
<define>
<field>ID</field>
<type>int(11)</type>
<null>NO</null>
<key>PRI</key>
<default/>
<extra>auto_increment</extra>
</define>
<define>
<field>Name</field>
<type>char(35)</type>
<null>NO</null>
<key/>
<default/>
<extra/>
</define>
<define>
...
</define>
</schema>
|
清单 21. 包含表记录的 XML 文档
<?xml version="1.0" encoding="UTF-8"?>
<data>
<record>
<id>1</id>
<name>Kabul</name>
<countrycode>AFG</countrycode>
<district>Kabol</district>
<population>1780000</population>
</record>
<record>
<id>2</id>
<name>Qandahar</name>
<countrycode>AFG</countrycode>
<district>Qandahar</district>
<population>237500</population>
</record>
<record>
...
</record>
</data>
|
- 继续进行该循环迭代,将两个独立的 XML
文档
$schema 和 $data 导入到父 XML 文档 $xml 中。
在前一示例中所见的 XPath 提供了一种简单的方法从 $schema 和 $xml 中提取 XML 节点碎片;DOM 扩展的 importNode() 方法处理其余文档,方法是外科手术般地将这些碎片插入到 XML 树主干的适当位置。
清单 22 显示了最终输出的一个片段:
清单 22. 由清单 18 生成的最终 XML 输出
<?xml version="1.0" encoding="UTF-8"?>
<database>
<table name="City">
<schema>
<define>
<field>ID</field>
<type>int(11)</type>
<null>NO</null>
<key>PRI</key>
<default/>
<extra>auto_increment</extra>
</define>
<define>
<field>Name</field>
<type>char(35)</type>
<null>NO</null>
<key/>
<default/>
<extra/>
</define>
...
</schema>
<data>
<record>
<id>1</id>
<name>Kabul</name>
<countrycode>AFG</countrycode>
<district>Kabol</district>
<population>1780000</population>
</record>
<record>
<id>2</id>
<name>Qandahar</name>
<countrycode>AFG</countrycode>
<district>Qandahar</district>
<population>237500</population>
</record>
...
</data>
</table>
<table>
...
</table>
</database>
|
清单 23 给出的是由 XML_Query2XML 类的开发者 Lukas Feiler 提供了一个更优雅的解决方案:
清单 23. 生成数据库结构和内容的 XML 清单的另一种方法
<?php
ini_set('max_execution_time', 120);
// credit: Lukas Feiler, http://www.lukasfeiler.com
// include files
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';
// initialize MDB abstraction layer
// load MDB manager
$mdb2 = MDB2::factory('mysql://root:pass@localhost/world');
$mdb2->loadModule('Manager');
// initialize Query2XML object
$q2x = XML_Query2XML::factory($mdb2);
// get table list
$tables = $mdb2->listTables();
// dynamically generate $options array
// once for each table
$elements = array();
for ($i=0; $i<count($tables); $i++) {
$elements['table' . $i] = array(
'rowTag' => 'table',
'attributes' => array(
'name' => ':' . $tables[$i]
),
'elements' => array(
'record' => array(
'idColumn' => false,
'sql' => 'SELECT * FROM ' . $tables[$i],
'elements' => array(
'*'
)
)
)
);
}
// get data from tables as XML
$xml = $q2x->getXML(
false,
array(
'idColumn' => false,
'rowTag' => '__tables',
'rootTag' => 'database',
'elements' => $elements
)
);
// write output to disk
// print success/error message
$xml->formatOutput = true;
if ($xml->save('/tmp/dump.xml')) {
echo 'Data successfully saved!';
} else {
echo 'Data could not be saved!';
}
?>
|
此解决方案首先加载 MDB2 Manager 模块,然后使用该模块的 listTables() 方法以一种数据库独立的方式检索数据库中所有表的列表。然后迭代此表列表,动态地针对每个迭代生成一个新的 elements 数组。一旦处理完所有的表,使用动态生成的 elements 数组调用 getXML(),生成整个数据库的 XML 转储文件并将其写入磁盘。清单 24 给出了输出文件中的一个片段:
清单 24. 由清单 23 生成的 XML 输出
<?xml version="1.0" encoding="UTF-8"?>
<database>
<table name="city">
<record>
<id>1</id>
<name>Kabul</name>
<countrycode>AFG</countrycode>
<district>Kabol</district>
<population>1780000</population>
</record>
<record>
<id>2</id>
<name>Qandahar</name>
<countrycode>AFG</countrycode>
<district>Qandahar</district>
<population>237500</population>
</record>
...
</table>
<table name="country">
<record>
<code>AFG</code>
<name>Afghanistan</name>
<continent>Asia</continent>
<region>Southern and Central Asia</region>
<surfacearea>652090.00</surfacearea>
<indepyear>1919</indepyear>
<population>22720000</population>
<lifeexpectancy>45.9</lifeexpectancy>
<gnp>5976.00</gnp>
<gnpold/>
<localname>Afganistan/Afqanestan</localname>
<governmentform>Islamic Emirate</governmentform>
<headofstate>Mohammad Omar</headofstate>
<capital>1</capital>
<code2>AF</code2>
</record>
...
</table>
<table>
...
</table>
</database>
|
结束语
正如前面这些清单所演示的,XML_Query2XML 包可以实现的功能远不止仅将 SQL 结果集转换为 XML。它可以用作各种应用程序的启用程序,范围涵盖了从简单的 SQL-to-HTML 转换程序到利用各种输入源(包括 Web 服务、磁盘文件和多个数据库系统)创建复杂 XML 文档的各种工具。出于以上原因,值得将其添加到 PHP 开发人员的工具箱。下次当您需要在 PHP/XML 应用程序和 SQL 数据库之间创建接口时试试这个包,自己体验一下效果!
参考资料 学习
获得产品和技术
讨论
关于作者
对本文的评价
|  |