级别: 初级 Kevin Williams (kevin@blueoxide.com), CEO, Blue Oxide Technologies,LLC
2002 年 1 月 01 日 在本篇专栏文章中,Kevin Williams 讨论了在 XML 中建模多对多关系的某些选项。本文讨论了几种不同的技术以及它们各自的优缺点。而且还提供了一些 XML 示例。
就其性质而言,关系数据库比层次数据存储结构(如 XML)更灵活。许多在关系数据库中很容易建模的关系(如发货系统中发票和零件之间的关系)结果在 XML 中进行建模却会变得相当难。在本篇专栏文章中,我将讨论一个典型的多对多建模问题,并研究为那种信息创建 XML 模型时的某些选项。
典型的建模难题
如果您有关系数据库数据的建模经验,那么您就会知道不同关系实体之间的多对多关系随时会出现。本专栏文章使用一个常用示例作为出发点:发货系统中的发票和零件。这是多对多关系的经典示例:发票可以包括许多零件,而每种零件可以出现在许多发票上。此外,您也许还有与关系本身相关的需要建模的其它信息。例如,当零件出现在发票上时,它通常会有与之相关的数量和价格。要在关系数据库中建模此信息,您也许会创建以下设计:
关系数据库中的样本表和列
Invoice
InvoiceID
CustomerName
InvoiceDate
ShippingMethod
InvoiceAmount
Part
PartID
PartCode
PartDescription
InvoicePart
InvoiceID
PartID
Price
Quantity
|
这证明是非常灵活的:如果想要查明在某一特定发票上有什么零件,或者多少发票包含了某一特定零件,可以只编写一个连接查询(使用主键和外键)以返回您感兴趣的信息。但是,当尝试在 XML 中建模此信息时,会遇到障碍。您也许会有一个包含
InvoicePart 元素的
Invoice 元素,但然后该怎么办?如果在
InvoicePart 元素中包含零件信息,那么此信息会在每个包含该零件的发票中重复!同样,如果选择使用一个包含
InvoicePart 元素的
Part 元素,那么在哪里表示
Invoice 数据呢?
在 XML 中建模这种类型的关系数据的关键是要了解您有可使用的所有工具,并应用常识来确定什么工具最适合某种特定情况。让我们来研究在 XML 中建模此信息的一些选项。
建模工具集
XML 为表示元素间关系提供了几种不同的机制。最常用的机制是父-子关系。这可以用于表示元素之间的一对一或一对多关系。但是,当试图表示多对多关系时,这种机制就不适合了,因为每个元素只有一个父元素。
XML 中的关系也可以用
ID-IDREF(S) 属性表示。通过使用这些属性,元素可以引用一个或多个其它元素(通过在指向元素自己的
IDREF 或
IDREFS 域中包含那些元素的标识字段的值)。虽然这也许看来直接类似于关系数据库的键机制,但是有一个重要的差异:大多数解析器都将这些指针看作是单向的。换句话说,给定一个
IDREF 或
IDREFS 域,可能会迅速找到有一个或多个相关标识的一个或多个元素,反之则不行。在我讨论建模解决方案时您会看到,结果成为这种设计的真正障碍。
既然拥有了这两个关系建模工具,现在让我们研究如何在 XML 中对发票和零件信息建模。
一些可能的解决方案
有几种方法可以在 XML 文档中处理这个建模问题。对于任何数据建模,不仅要考虑在文档中表示信息的最有效方式,而且要考虑文档的读者是谁以及如何使用该文档,这都是有好处的。
废弃信息
该问题最简单的解决方案是只要废弃某些多对多关系通常需要通过链接的信息。这种调用通常是根据文档的预期读者而进行的。例如,如果您正在向客户发送用做开票目的的发票汇总,那么也许不需要包括零件信息。同样,如果正在创建要用作零件数据库的文档,那么在发票上如何对零件排序的细节也许并不重要。
消除多对多关系
另一种可能性是限制文档的作用域,以使多对多关系消失。在此方案中,文档被限制成描述参与多对多关系的元素之一,而不是尝试在一个文档中包含数据库中的所有信息。例如,我们也许会决定为每张发票创建一个 XML 文档。在这种情况下,以下 XML 文档就足够了:
限制成单张发票的样本文档
<invoice
customerName="John Q. Anybody"
invoiceDate="1/7/2002"
shippingMethod="UPS"
invoiceAmount="29.55">
<part
partCode="X1Y23"
partDescription="Grommet, steel, 3-inch"
price="0.25"
quantity="72" />
<part
partCode="Y2Z29"
partDescription="Sprocket, brass, 2-inch"
price="0.35"
quantity="33" />
</invoice>
|
请注意,您已经将
Part 和
InvoicePart 表中的信息一起合并到一个元素 ―
part ― 中,因为
Part 和
InvoicePart 之间的关系现在由于该文档的需要而变成一对一关系。
这种方法使得我们易于创建描述数据子集的 XML 文档,但是它要以牺牲灵活性为代价。试图汇总某特定发票日期的零件订单的程序员不得不解析许多文档,并且手工聚集数据 ― 这是一项不值得羡慕的任务。但是,这种方法通常可以产生满足某特定目标的最佳结果。
使用单一 IDREF 关系
如果需要保留信息的所有细微差别而不丢失数据点之间的关系会发生什么呢?最显而易见的解决方案是使用单一
IDREF 关系将相关元素指回到它需要引用的元素。例如,您可以创建类似于以下文档的结构:
使用单一 IDREF 的样本文档
<shippingData>
<invoice
customerName="John Q. Anybody"
invoiceDate="1/7/2002"
shippingMethod="UPS"
invoiceAmount="29.55">
<invoicePart
partIDREF="X1Y23"
price="0.25"
quantity="72" />
<invoicePart
partIDREF="Y2Z29"
price="0.35"
quantity="33" />
</invoice>
<invoice
customerName="Michael X. Somebody"
invoiceDate="1/8/2002"
shippingMethod="FedEx"
invoiceAmount="22.00">
<invoicePart
partIDREF="X1Y23"
price="0.25"
quantity="88" />
</invoice>
<part
partID="X1Y23"
partDescription="Grommet, steel, 3-inch" />
<part
partID="Y2Z29"
partDescription="Sprocket, brass, 2-inch" />
</shippingData>
|
请注意,即使有两张包括垫圈的发票,但关于垫圈的细节在实际文档中只包含一次。这允许您降低文档大小,同时又保留了所需要的所有数据丰富性。但是,此文档大小减缩是有代价的:解析文档会明显变得更困难,尤其是使用如 SAX 等流型解析器时。同样,
IDREF 指针的单向性质也成为了一个问题:如果要汇总某张特定发票中的零件,那么该文档会正常工作,但假定想要汇总包含某特定零件的发票呢?因为不能方便地从某个
ID 导航到某个
IDREF ,因此这个文档并不适合那个应用程序。此设计代表对多对多问题的经典解决方案。而且,如果愿意忍受较大一点的文档大小带来的开销,那么就可以真正对它加以改进(从灵活性的观点)。
使用双重 IDREF 关系
在此设计中,有两个不同的
IDREF -到-
ID 关系:一个从相关元素指向关系中的非父元素参与者,另一个从关系中的非父元素参与者指向相关元素(这必须是
IDREFS 属性)。这里是同一个示例,它是使用双重指针设计的:
使用双重 IDREF 的样本文档
<shippingData>
<invoice
customerName="John Q. Anybody"
invoiceDate="1/7/2002"
shippingMethod="UPS"
invoiceAmount="29.55">
<invoicePart
invoicePartID="IP1"
partIDREF="X1Y23"
price="0.25"
quantity="72" />
<invoicePart
invoicePartID="IP2"
partIDREF="Y2Z29"
price="0.35"
quantity="33" />
</invoice>
<invoice
customerName="Michael X. Somebody"
invoiceDate="1/8/2002"
shippingMethod="FedEx"
invoiceAmount="22.00">
<invoicePart
invoicePartID="IP3"
partIDREF="X1Y23"
price="0.25"
quantity="88" />
</invoice>
<part
invoicePartIDREFS="IP1 IP3"
partID="X1Y23"
partDescription="Grommet, steel, 3-inch" />
<part
invoicePartIDREFS="IP2"
partID="Y2Z29"
partDescription="Sprocket, brass, 2-inch" />
</shippingData>
|
这里,您能够从两个方向来导航多对多关系。如果要汇总某特定零件的发票,只要通过
invoicePartIDREFS 属性导航回到相应的
invoicePart 元素,然后从该元素导航到发票父元素。虽然这种方法仍会使解析很困难,但大多数解析器遍历新的指向关系比遍历旧关系更容易。这会使该文档略微更大一点,但却是本专栏文章任何示例中最灵活的。
结束语
您使用哪种解决方案呢?这完全取决于文档所针对的读者。如果文档有一个已知目的(例如,它是为样式化而立即(on the fly)生成的),那么您可以采用我描述的任意一种快捷方式,使该文档更易于操作以便实现其预定目的。如果文档意图只是过渡文档(例如,在使用信息之前会将信息放入关系模型中),那么您可以带走不太灵活的结构。但是,如果文档确定是用作某信息的特定子集的记录数据,那么就应该使它尽可能简单以便访问该文档的程序抽取信息 ― 以及信息之间的关系。
参考资料
关于作者
对本文的评价
|