级别: 初级 David Mertz,Ph.D. (mertz@gnosis.cx), 格式操纵员, Gnosis Software, Inc.
2001 年 6 月 01 日 前面有一篇专栏研究了从 SQL 查询生成 XML 文档。现在,David Mertz 说明将 XML 文档和 DTD 反向转换成 RDBMS 存储格式也同样可能,但它有自己的约束和复杂性集合。Python 公众域利用了这里所讨论的
xml2sql 和
dtd2sql 生成 SQL 语句,以一种一致和可逆的方式创建和填充数据库。这里使用了 7 个代码示例演示了这些技术。
XML 文档和 RDBMS 都将在相当长的一段时间内存在。这是我在研究了各种数据模型并了解 XML 是如何成为其中一部分以前(以及目前)而得出的结论。这里对一些实用程序的讨论继续帮助开发人员更方便地进行数据和格式转换。通过使用
dtd2sql 和
xml2sql ,程序员可以
自动将 XML 文档的内容移到 SQL 数据库中(以及稍后检索出信息)。
告诫
我希望,在这里以及在早期讨论
sql2dtd 和
sql2xml 时所介绍的实用程序,会让开发人员在寻找 XML 和 SQL 之间快速而实际的转换的工作更轻松。但不要假定这些实用程序可以替代数据库分析师。优化、规范化和反向规范化是一些需要知识和经验的复杂问题。我所介绍的工具提供了一些良好、可用和易于部署的解决方案;但这些解决方案只是有些时候是定制开发的替代。
而且,
xml2sql 和
dtd2sql 最适合于包含从面向表信息开始的 XML 文档。对于面向散文和面向线性的 XML 文档的转换,它们则显得苍白无力。在这方面所强加的限制本质上等同于
xml2sql 利用的
xml_objectify 库所强加的限制。不过,这一特殊焦点严格来说算不上是限制:实际上,
任何技术 -- 就是定制开发 -- 都无法在 RDBMS 框架中产生非常自然的散文数据和线性数据的表示。各个模型均不相同,
xml2sql 和任何实用程序一样只能做到大致良好的地步。
最后,不要指望
xml2sql 特别快或者特别有效;这种实用程序主要是为可移植性和通用性而设计的。因此,不调用任何数据库的库 -- 无论是特定的 RDBMS 还是通过象 ODBC 这样的机制 --
xml2sql 和
dtd2sql 都创建简单的文本 SQL 语句。模块
dtd2sql 生成
CREATE TABLE 语句的列表,而
xml2sql 生成
INSERT INTO 语句的列表。是否将这些语句放入实际的 RDBMS 由用户或程序员决定。这种安排的好处在于,开发人员可以同样好地将这些语句放入任何供应商的 RDBMS。
尝试一下
将模块
xml2sql 和
dtd2sql 结合实际示例一起介绍可能效果最好。稍后我会转回到设计中的一些理论问题。
出于测试目的,我创建了一个简单的测试脚本。出于开发目的,我使用流行的开放源码 RDBMS mySQL。虽然与更复杂的 RDBMS 相比, mySQL 有一些限制,它提供了一个非常好的测试平台。样本 DTD 和 XML 文档属于我所编写的 developerWorks 教程(请参阅
参考资料中的集合)。清单 1 中的脚本是 OS/2 命令文件,但在 Windows 下也应该一样有效,在类 Unix 的系统上只需要少许修改。
清单 1. 将 XML 传送到 SQL 所使用的测试脚本
echo drop database test; > test.sql
echo create database test; >> test.sql
echo use test; >> test.sql
python dtd2sql.py dwtut.dtd >> test.sql
python xml2sql.py haskell.xml >> test.sql
mysql -u root -pPASSWORD test < test.sql
|
清单 1 中脚本的前面几行只是清除并恢复 mySQL 中的
test 数据库。在现实生活中,您不太会
drop 现有的数据库,而只是从表中
INSERT 和
DELETE 。我使用
drop 是因为在测试时最好重新开始。
运行
dtd2sql 时,它从 STDIN 或从命令行上给出的文件名中获得 DTD。这个 DTD 可能是 XML 文档的内部子集,如果愿意,它也可以是外部文件。不过,这个工具一次只能从单一源中读取,它当前不处理复杂的多文件 DTD、参数实体或部分覆盖外部定义的内部子集。前面提到过,
dtd2sql 只产生一组
CREATE TABLE 语句。
在清单 2 中,可以看到来自
dtd2sql 的一个输出行,然后理解如何将其各个部分拆解(为显示起见添加了一些折行)。
清单 2. 来自 dtd2sql 的样本 CREATE TABLE 语句
CREATE TABLE a (
primary_key BIGINT UNSIGNED PRIMARY KEY,
seq INT UNSIGNED,
href BLOB,
PCDATA BLOB,
_XML BLOB,
foreign_key_p BIGINT UNSIGNED,
foreign_key_li BIGINT UNSIGNED,
foreign_key_prompt BIGINT UNSIGNED,
foreign_key_response BIGINT UNSIGNED
);
|
某些列名很容易由
a
元素本身的定义解释;对于其它的,则需要加以进一步研究。
清单 3. <a> XML 元素的 DTD 项
<!--A hyperlink to some other resource.-->
<!ELEMENT a (#PCDATA | code)* >
<!ATTLIST a href CDATA #REQUIRED >
|
每个
CREATE TABLE 语句都包括一个
primary_key 和
seq 列。
seq 列有时没什么意义(除了表明缺少属于某些行的顺序性)。用户在命令行上或通过定制应用程序运行这样的
CREATE TABLE 语句将在每个表中创建这些列。
href 列直接来自同样命名 XML 的标记属性。
PCDATA 和
_XML 列存放的是实际的元素内容(有或没有任何嵌入式字符级标记)。
所示的
CREATE TABLE 语句中最有意思的是几个
foreign_key_* 列。我会在下面考虑这些。
创建关系
按照关系模型,不同的表是通过主键/外键标识相互连接的。SQL 中的 JOIN 只是简单说明一个表中的字段必须对应于另一个表中的另一字段的方法。在关系模型中
主键是特殊的事物:它对于表的每个记录(行)必须是唯一的。大多数时候,数据库分析师查看数据的深层结构并指出 -- 在咨询了应用程序员和最终用户后 -- 哪些是充当主键的最佳候选。这些键可以是多个列的并置,通常象“社会保障号”、雇员标识、ISBN 或部件标识这样的标识用于这些角色中。
很明显,
dtd2sql 无法执行数据库分析师所执行的所有背景研究:它有的只是 DTD(也可能是 XML 文档)。在这个基础上,没有一种真正的方法可以确定哪些属性或元素内容是唯一的(如果有的话)。幸好,
dtd2sql 可以采取某些商业 RDBMS 的途径,这样就能轻易地避开“天然的”主键。模块能够选择完全人造的主键 -- 在拒绝所有数据表示角色时分解出唯一性需求的那些主键。我不认为这样的模式在数据库设计中能够实际达到比标识适合“现实世界”数据的更常用策略更好的正交性。而且这种方法在为每个表的主键提供完全一致和可预测的名称以及格式方面有着额外的优势。
使用的主键是随机 18 位整数。假设 Python 的
random 模块相当好,冲突的风险也非常小。不过,代码并不能严格保证无冲突(可能在以后的版本中可以)。到目前为止还不错。
下一步是使这些主键可用于 SQL JOIN 目的。要这样做,需要确保当 XML 元素包含子元素时,子元素包含与父代主键相对应的外键。不可否认,达到上述目的最节约的方法是为每个非根的 XML 元素创建一个
foreign_key 列。在那种情况下,查询合成数据库的 SQL 用户需要
知道哪些 JOIN 会产生结果(例如通过读取原始 DTD)。
抛开节约,我选择明确性。我为每个
可能是与表所对应的 XML 元素父代的元素创建了单独的
foreign_key_* 列。所以在上述
CREATE TABLE 示例中,
dtd2sql 标识了在清单 4 中显示的 DTD 元素定义。
清单 4. 可能是 <a> 的父代的元素
<!ELEMENT p (#PCDATA | code | img | br | i | b | a)* >
<!ELEMENT li (#PCDATA | code | img | br | i | b | a)* >
<!ELEMENT prompt (#PCDATA | code | img | br | i | b | a)* >
<!ELEMENT response (#PCDATA | code | img | br | i | b | a)* >
|
dtd2sql 明确方法的一个好处是,所创建的表结构固有地在 DTD 中包含大部分信息(但并非全部,因为量词并不因此而有分别)。
走些弯路
将数据放入由
dtd2sql 创建的表中是
xml2sql 的任务。当然,从技术上说,这两种工具实际上都没有将任何数据放在任何地方;每个工具都只是指定数据是什么。需要使用 RDBMS 所带的工具来实际装入数据。
事实是
xml2sql 做的非常少。其核心(
walkNodes() 函数)代码连 50 行都没有。而且,即使这几行文档都编制得非常详细,无法通过编程技巧来达到简明性。当然,
xml2sql 所执行的大部分任务实际上是由
xml_objectify 完成的。
xml2sql 的第一步是使用
xml_objectify 创建一个“Python 化”的对象。然后,要遍历所有嵌套的属性就很简单了,在进行中输出
INSERT INTO SQL 语句。不过,旧版本
xml_objectify 的用户需要获取最新的版本,因为 XML 元素命名方式的细小更改造成了一路“破坏”。
一旦运行了
xml2sql ,您会得到一束 SQL 语句作为回报。这组语句按照正常的 STDOUT 行为可以被重定向和导向管道,使得
xml2sql 与 RDBMS 命令行工具的结合非常直截了当。如果希望使用
xml2sql 作为支持模块,可以获得以 Python 列表获取该组 SQL 语句(可能对于和某些数据库模块一起使用很方便)。产生的典型语句看起来如清单 5 中的示例所示(为显示起见进行了折行)。
清单 5. 产生的典型语句
INSERT INTO p
(primary_key, seq, foreign_key_text__column, PCDATA)
VALUES (15447926390024014, 0, 527610371062647168,
"Navigating through the tutorial is easy:");
|
可以从清单 5 中
INSERT INTO 的形式看出,对应 DTD 中的
<p> 元素创建了一个表。实际上,我们只是知道该元素出现在 XML 文档中;对照 DTD 确认 XML 文档是需要在这些模块外部处理的作业。但假设 XML 文档是有效的,
dtd2sql 可以创建正确的表和列。
还可以在
INSERT INTO 中看到这个特殊的
<p> 标记是嵌套在
<text-column> 元素内部的(需要对某些名称做大改动才能获得有效的 SQL 列名),即,具有
527610371062647168 主键的元素。它也证明了这个
<p> 元素具有某些 PCDATA 内容,它的
seq 列值为零。该列表位的含义是,
<p> 元素独立位于其容器中;如果在同一个
<text-column> 中出现多个
<p> 元素,它们就按顺序排列,从 1 开始。
组合起来
在 RDBMS 中有了一束数据后,通常希望以结构化和有用的方式检索它们。幸运的是,随着对使用的主键和外键策略的基本理解,可以找到您所需的全部内容。实际上,通过许多方式,您在这时所具有的灵活性比使用 XPath 查询语法可能具有的灵活性要
好很多。请看清单 6 中的示例。
清单 6. 从 RDBMS 中选择数据
SELECT "Paragraph", p.seq, p._XML
FROM title,panel,body,text__column TC,p
WHERE title.foreign_key_panel = panel.primary_key
AND body.foreign_key_panel = panel.primary_key
AND TC.foreign_key_body = body.primary_key
AND p.foreign_key_text__column = TC.primary_key
AND title.PCDATA="About Haskell"
ORDER BY p.seq
;
|
有必要进行一些解释。JOIN 的构成都一样:
foreign_key_X 字段与某些表
X 的
primary_key JOIN 起来。一旦所有 JOIN 都就位后,可以添加 ORDER、GROUP 等等更实质性的条件。在这种情况下,您希望查看其
<title> 是 "About Haskell" 的
<panel> 的所有 paragraph(段落)(
<p> 元素)。结果看上去如清单 7 所示。
清单 7. 针对 XML 教程的 SQL 查询
C:\mysql2\bin>mysql -u root -pgnosis test < haskell.sql
Paragraph seq _XML
Paragraph 1 Haskell is just one of a number of functional programming...
Paragraph 2 Among functional languages, Haskell is in many ways the...
Paragraph 3 On a minor note, Haskell is syntactically easier to get...
|
结束语
本专栏阐明了
dtd2sql 和
xml2sql 的命令行用法。对于快速测试和实际的 shell 用法,这可能是您希望使用的方法。不过,和 Python 中的大多数事物一样,在自己的代码中重用这些模块是非常简单的。自测代码(命令行用法)为任一导入模块提供了一个可遵循的简单模板。我期待听到读者准备将这些模块用在一些很棒的用途 -- 就和您为许多其它人所做的一样。
参考资料
通过单击本页顶部或底部的
讨论来询问问题或对本文加以评论。
关于作者
对本文的评价
|