簡介
XML 在數據庫中的地位在過去兩年中已經發生了變化,從 “臨時工” 變成了重要成員。它不再需要改變本身來適應關系環境。它可以保持其層次化性質,同時利用關系數據庫環境的功能和穩定性。實際上,一些關系性元素已經采用某些技術讓它們看起來像 XML,以便利用層次化 XML 模型豐富的功能。
本文討論新的 XML 存儲和查詢環境如何處理本系列 第 1 部分 中的 XML 數據模型。還要說明,在采用新的基於 XML 的應用程序開發體系結構之後,數據庫模式會變得更簡單更自然。還演示如何按照在應用程序中查詢數據的相同方式查詢數據庫中的 XML 數據。最後,討論如何結合關系數據和 XML 數據,從而同時獲得這兩個環境的優勢。
XML 數據庫基礎
盡管大多數主流關系數據庫都有某種 XML 支持,但是 DB2 的 pureXML™ 支持要健壯和高效得多,這使它成為試驗 XML 編程模型的理想數據庫。本文主要關注如何在應用程序體系結構中使用新的 XML 數據庫支持。
DB2 允許存儲、查詢、操作和發布:
關系數據 — SQL
采用 XML 形式的關系數據 — SQL/XML
XML 數據 — XQuery
混合型數據(關系數據和 XML 數據) — SQL/XML 和 XQuery
圖 1. DB2 混合型存儲
在數據庫中存儲 XML
關系數據庫中的 XML 支持的主要好處是,可以在同一個表中同時存儲關系數據和 XML 數據。另外,盡管 XML 在內部存儲為層次化(樹)格式,但是它看起來像是存儲在數據庫表的單一列中(就像 CLOB 或 BLOB)。
從第 1 部分中的數據對象可以看出,有兩個表,每個表至少有兩列。
清單 1. 表CREATE TABLE CUSTOMER_TABLE (
CUSTOMERID CHARACTER (12) NOT NULL,
CUSTXML XML NOT NULL ,
CONSTRAINT CC1183665042494 PRIMARY KEY ( CUSTOMERID) )
CREATE TABLE PURCHASE_TABLE (
CUSTOMERID CHARACTER (12) NOT NULL ,
ITEMXML XML NOT NULL ,
CONSTRAINT CC1183665244645 FOREIGN KEY
(CUSTOMERID) REFERENCES CUSTOMER_TABLE (CUSTOMERID)
ON DELETE CASCADE ON UPDATE NO ACTION
ENFORCED ENABLE QUERY OPTIMIZATION )
顯然,通過將應用程序的數據對象存儲為 XML,關系模式大大簡化了。另外,基礎結構仍然是關系型的,這使 XML 數據能夠利用關系數據庫的實用功能,比如觸發器、約束和外鍵關系。
因為從邏輯上看 XML 列與 VARCHAR、CLOB 或 BLOB 列相似,所以 INSERT 語句也是相似的。
insert into CUSTOMER_TABLE values('hardeep',
'<Customer customerid="hardeep" firstname="hardeep" lastname="singh"/>')
在 Java™ 程序中執行插入的代碼也是相似的:
清單 2. 在 Java 程序中執行插入String insertsql= "insert into PURCHASE_TABLE values(?,?)";
PreparedStatement iStmt=connection.prepareStatement(insertsql);
File inputfile= new File(filename); //filename is the path of the XML file
long filesize=inputfile.length();
BufferedReader in = new BufferedReader(new FileReader(inputfile));
iStmt.setCharacterStream(1,in,(int)filesize);
int rc= iStmt.executeUpdate();
為了更好地了解混合型存儲,我們來看看 XML 數據的邏輯視圖,體會 XML 數據如何看起來像是存儲在關系數據庫表中。
注意:盡管不同關系數據庫廠商的 XML 物理存儲技術可能不一樣,但是邏輯視圖是相似的。
圖 2. DB2 混合型存儲邏輯視圖
圖片看不清楚?請點擊這裡查看原圖(大圖)。
查詢 XML
在展開數據庫模式模型時,可以看到關系表和列。如果展開 XML 列,模式會從關系模型變成 XML 層次化模型。現在,您應該意識到其實有兩個模式(一個關系模式和一個 XML 模式),但是把它們當作一個整體;理解了這一點,就能夠以更自然的方式在這個統一的模式中進行導航和查詢。
對於清單 1 所示的統一模式,如果希望獲得 CUSTOMER_TABLE 中 CUSTXML 列的數據,那麼可以在查詢中指定 CUSTXML 列的路徑作為目標。
SELECT CUSTXML FROM CUSTOMER_TABLE where customerid='hardeep';
這會返回 hardeep 的 CUSTXML 列中的客戶數據。
現在,考慮如何獲得其 lastname 為 singh 的客戶數據。在這種情況下,需要指定每個 XML 文檔的 lastname 屬性的路徑(CUSTOMER_TABLE.CUSTXML/Customer/@lastname)並檢查它是否是 singh。
在完美的環境中,查詢應該是 Select * from CUSTOMER_TABLE where CUSTXML/Customer/@lastname='singh'。但是在真實環境中,需要用數據庫查詢引擎能夠理解的一種語法編寫查詢。數據庫領域已經引入了一種稱為 XQuery 的新語言,可以用它查詢 XML 文檔。SQL 添加了可以理解這種語言的新函數,從而將關系和 XML 環境聯系起來了。所以搜索姓氏為 singh 的客戶的查詢如下:
select CUSTXML from CUSTOMER_TABLE
where xmlexists ('$cust/Customer[@lastname= "singh" ]' passing CUSTXML AS "cust" )
還可以在 Java 程序中通過參數化查詢執行這個調用:
select CUSTXML from CUSTOMER_TABLE
where XMLexists ('$cust/Customer[@lastname= $lname ]'
passing CUSTXML AS "cust" , cast(? as VARCHAR(12)) as "lname")
掌握了向 SQL/XML 函數傳遞參數的語法之後,您會發現,在針對關系數據和 XML 數據的基本混合型查詢中,XML 查詢通常包含 XPath 表達式。這與在應用程序層中操作 XML 數據模型的方法非常相似(見第 1 部分),在那裡許多代碼通過對 Document Object Model(DOM)包裝器進行 XPath 調用來查詢和操作 XML 數據。
注意:在 Viper 2 中,對傳遞給一些 SQL/XML 函數的參數做了簡化。例如,在前面的查詢中,XMLExists 的 passing 子句不需要指定 CUSTXML 列。
select CUSTXML from CUSTOMER_TABLE
where xmlexists ('$CUSTXML/Customer[@lastname= $lname ]'
passing cast(? as VARCHAR(12)) as "lname")
將應用程序邏輯放在數據庫中
XQuery 提供了大多數高級語言的所有基本功能(if-then-else、for、變量、函數和算術操作符)。因此,可以將業務邏輯嵌入查詢中。另外,它還提供許多常用的 XSLT 映射,所以它不但能夠執行查詢,還能夠在數據庫中轉換 XML 輸出。
我們仍然以第 1 部分中 Customer 示例的 XML 數據模型為例。
<Customer customerid ="" firstname="" lastname="" >
<Items><Item ID="" description="" purchaseDate="" price="" /></Items>
</Customer>
用 DB2 查詢替代應用程序代碼
我們不必在應用程序層中合並來自兩個表的 XML 數據,可以在數據庫中用一個 SQL/XML 查詢實現相同的效果。這需要一個 CUSTOMER_TABLE.CUSTXML/Customer/@customerid 到 PURCHASE_TABLE.ITEMXML/Customer/@customerid 的一對多聯結。
圖 3. 聯結兩個 XML 列
圖片看不清楚?請點擊這裡查看原圖(大圖)。
清單 3. 查詢兩個 XML 列
values(XMLquery('
for $Customer in db2-fn:xmlcolumn( "CUSTOMER_TABLE.CUSTXML")/Customer
where $Customer/@customerid= $customerid
return
<Customer customerid ="{$Customer/@customerid}"
firstname ="{$Customer/@firstname}" lastname ="{$Customer/@lastname}" >{
for $Customer0 in db2-fn:xmlcolumn("PURCHASE_TABLE.ITEMXML")/Customer
where $Customer0/@customerid= $Customer/@customerid
return $Customer0/Item
}</Customer>
' passing cast( ? AS varchar(255) ) as "customerid" ))
對於客戶 hardeep 購買的所有商品,產生的 XML 如下:
圖 4. 查詢結果
在上面的查詢中,必須構造外層的 Customer 元素並添加來自 CUSTXML 列數據的屬性。DB2 Viper 2(beta)支持 XQuery 更新表達式,可以修改 XML 文檔,所以不需要構造外層的 Customer 元素。可以使用來自客戶表的一個 XML 文檔,並將來自購買表的商品數據作為子元素插入其中。
清單 4. 針對兩個 XML 列的 Viper 2 查詢values(XMLquery('
for $Customer in db2-fn:xmlcolumn( "CUSTOMER_TABLE.CUSTXML")/Customer
let $items:=(<Items>{
for $Customer0 in db2-fn:xmlcolumn("PURCHASE_TABLE.ITEMXML")/Customer
where $Customer0/@customerid= $Customer/@customerid
return $Customer0/Item
}</Items>)
where $Customer/@customerid= $customerid
return
transform
copy $cust:=$Customer
modify(
do insert $items as last into $cust)
return $cust
' passing hardeep as "customerid" ))
在上面的查詢中,不但搜索、獲取和合並了數據庫中存儲的 XML 文檔部分,還在產生的 XML 中添加了新元素。另外,hardeep 被隱式轉換為 XML 類型(xs:string)。
對比數據庫查詢和 Java 應用程序代碼
如果將上面的查詢與第 1 部分中的 Java 代碼(清單 6. 重寫應用程序來使用 XML 模型)做一下對比,就會發現它們的邏輯非常相似。
從 CUSTOMER_TABLE 中選擇 Customer 信息。
構造一個 Items 元素並在 PURCHASE_TABLE 中搜索這位客戶購買的所有商品。
循環遍歷列表中的每個商品並將它們插入 Items 元素。
將 Items 元素插入 Customer 元素。
創建存儲過程
為了將數據庫中的業務邏輯與應用程序代碼隔離開,一種好方法是為這個查詢創建一個存儲過程。
清單 5. 創建存儲過程CREATE PROCEDURE customerItems(IN custid varchar(12))
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
DECLARE c_cur CURSOR WITH RETURN FOR
values(XMLquery('
for $Customer in db2-fn:xmlcolumn( "CUSTOMER_TABLE.CUSTXML")/Customer
let $items:=(<Items>{
for $Customer0 in db2-fn:xmlcolumn("PURCHASE_TABLE.ITEMXML")/Customer
where $Customer0/@customerid= $Customer/@customerid
return $Customer0/Item
}</Items>)
where $Customer/@customerid= $customerid
return
transform
copy $cust:=$Customer
modify(
do insert $items as last into $cust)
return $cust
' passing custid as "customerid" ))
OPEN c_cur;
END
用存儲過程調用替代應用程序代碼
應用程序代碼現在可以對 DB2 進行存儲過程調用並將 XML 傳遞給 DOM 包裝器。第 1 部分中的 XML 數據模型的應用程序代碼(清單 6. 重寫應用程序來使用 XML 模型,2-8 行)可以簡化為:
2. ResultSet dbResult = dbstmt.executeQuery("call customerItems ("+custid+")"
3. XMLParse customerXML = new XMLParse(dbResult. getString(1));
一個更精細的示例
現在考慮一個比較精細的場景,這個場景還要計算每個商品的保險費。為了讓這個場景有點兒難度,保險費不但每天變動,而且隨價格變化。這意味著,不但要向查詢傳遞 customerid,還要傳遞保險費率。現在,假設您每天在保險公司提供的一個 Web 服務中查詢最新的保險費率。保險費率信息采用 XML 文檔的形式。
<insurance>
<rate price="100" currency="$" rate=".02"/>
<rate price="500" currency="$" rate=".018"/>
<rate price="" currency="$" rate=".015"/>
</insurance>
可以修改前面的存儲過程來計算保險費。
清單 6. 計算每個商品的保險費的存儲過程CREATE PROCEDURE customerItemsWithInsurance(IN custid varchar(12), rate XML)
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
DECLARE c_cur CURSOR WITH RETURN FOR
values(XMLquery('
for $Customer in db2-fn:xmlcolumn( "CUSTOMER_TABLE.CUSTXML")/Customer
let $items:=(
<Items>{
for $Customer0 in db2-fn:xmlcolumn("PURCHASE_TABLE.ITEMXML")/Customer
let $insurance:=<insurance currency="{($rate//rate[@price=""]/@currency)}">
{(
if($Customer0/Item/@price > 500) then (
$Customer0/Item/@price * $rate//rate[@price=""]/@rate
)
else (
if($Customer0/Item/@price > 100) then (
$Customer0/Item/@price * $rate//rate[@price="500"]/@rate
)
else (
$Customer0/Item/@price * $rate//rate[@price="100"]/@rate
)
)
)}</insurance>
where $Customer0/@customerid= $Customer/@customerid
return
transform
copy $item:=$Customer0/Item
modify( do insert $insurance as last into $item)
return $item
}</Items>
)
where $Customer/@customerid= $customerid
return
transform
copy $cust:=$Customer
modify(do insert $items as last into $cust)
return $cust
' passing custid as "customerid", rate as "rate" ));
OPEN c_cur;
END
對這個存儲過程的調用接受兩個運行時參數,customerid 和保險 XML。
call customerItemsWithInsurance(?,?)
顯然,在上面的示例中,如果要操作的數據庫數據是 XML 格式的,那麼可以使用 XQuery 實現比只使用 SQL 時更多的業務邏輯。另外,查詢中使用的 XML 甚至不需要在數據庫中存在。因此,SQL/XML 查詢中涉及的 XML 數據可以按照層次化形式存儲在數據庫中,可以由 SQL/XML 函數生成,甚至可以作為運行時參數傳遞給查詢。數據庫和應用服務器之間的差異逐漸變得模糊了。
優點和缺點
所有新技術在剛剛出現時都會有一些問題。一些問題是由於實現還不成熟造成的,其他問題是由於開發人員還不適應變化。
盡管性能有所改進,但是仍然比不上關系數據。
XQuery 是一種新語言,而且一些 SQL/XML 函數的語法需要花時間適應。
有許多遺留數據采用關系格式。
最重要的是,這是一種創建業務應用程序和數據模式的新方式,它與當前的面向對象應用程序和規范化關系模式很不一樣。
能夠對這類查詢進行調試和優化的工具還不多。
盡管存在這些缺點,但是新的模型管理數據的方式更加自然。在應用程序層和數據庫層中都按原樣維護和操作業務數據信息,而且您將第 3 部分中看到,甚至在客戶機層中也是如此。
盡管外圍語言可能不一樣(Java、XQuery、JavaScript、PHP),但是在所有層中用來在 XML 文檔中移動的語言都是相同的(XPath)。
即使遺留數據是關系型的,但是通過使用 Viper 2 中引入的一些 SQL/XML 函數,可以輕松地查詢這些數據並將它們轉換為 XML。看一下第 1 部分中的示例 “第二種情況 —— 所有數據在數據庫中存儲為關系形式”。可以使用 Viper 2 中引入的 XMLROW 函數簡化這個查詢。
Select XMLROW (customerid, firstname, lastname OPTION as attributes ROW Customer)
from customer_table where customerid=?
還可以在關系數據和 XML 數據之間創建聯結。在這個示例場景中,如果有第三個表,其中包含購買商品的產品說明,而且這是一個關系表,那麼可以使用商品 ID 執行聯結,從而獲得購買的每種商品的產品說明。
圖 5. 聯結關系列和 XML 列
Select details, weight from SQLPRODUCT, ITEM_TABLE
where xmlexists ('$itemXML/item[@itemid=$pid]'
passing ITEM_TABLE.ITEMXML AS "itemXML", SQLPRODUCT.PID AS "pid" )
在 DB2 9 中,可以使用 passing 子句向 SQL 語句中嵌入的 XQuery 傳遞運行時參數,但是不能向 XQuery 中嵌入的 SQL 傳遞參數。在 Viper 2 中,這個限制已經消除了,現在可以向 XQuery 中嵌入的關系查詢傳遞運行時變量。
清單 7. 向 XQuery 中嵌入的 SQL 傳遞運行時變量
values(XMLquery('
for $Customer0 in db2-fn:xmlcolumn("PURCHASE_TABLE.ITEMXML")/Customer
where $Customer0/@customerid= $custid
return (
$Customer0/Item,
db2-fn:sqlquery(
''select XMLrow(details, description, weight option ROW "description")
from sqlproduct where pid= parameter(1)'', $Customer0/Item/@ID))
' passing cast( ? AS varchar(255) ) as "custid" ))
因此,即使一部分數據位於關系表中,一些數據是 XML,現在都可以在 SQL 查詢、XQuery 或這兩者中在 XML 數據和關系數據之間進行動態聯結。
在某些情況下,性能可能不是大問題,因為:
能夠為數據庫中存儲的 XML 文檔創建基於 XPath 表達式的索引。
create index custfname on customer_table(info) generate key
using XMLpattern '/Customer/@firstname' as sql varchar(64)
因為數據庫模式更簡單,所以減少了所需的聯結數量。
因為現在可以在查詢中縮減數據,然後再把數據發送給應用程序,所以可以減少 I/O。
隨時可以使用 SQL/XML 函數(比如 XMLTable)將 XML 文檔中的關鍵信息提取到關系列中,並為它們創建關系索引。
可以為 XML 文檔創建文本搜索索引。
結束語
XML 已經取得了穩固的地位。大多數行業和政府組織都對他們的 XML 模式做了標准化,並要求電子文檔必須符合這些模式。既然通過線路交換的 B2B 數據已經采用了 XML 格式,為什麼不在數據庫中按原樣(pureXML)存儲這些數據呢?將數據存儲為 XML 之後,可以使用 XQuery 和標准的 SQL/XML 對它進行編制索引、查詢、檢驗、操作、轉換和更新。隨著越來越多的應用程序邏輯被放在查詢中,數據庫可以以 Web 服務和 feed 的形式公開它的存儲過程,從而成為面向服務體系結構(SOA)環境中活躍的參與者。
“舊的秩序已經改變了,新的秩序正在形成。”(The old order changeth, yIElding place to new.) Morte d'Arthur