XML 是當今 Web 最常用的數據交換格式之一。DB2® 對 pureXML™ 的支持以及與 parsing and generating XML API(REXML)的結合,為 Web 應用程序開發提供了強大的技術組合。DB2 數據服務器中的本地 XML 支持引入了極大的靈活性,通過使用 DB2 的混合型數據庫引擎編譯器和解析器(可同時使用 SQL 和 XQuery),能夠混合存儲半結構化的具有層次結構的 XML 文檔和關系數據。在Ruby on Rails 系列的第二篇文章中,我們將演示如何在第一篇文章的 Team Room 示例中利用 pureXML 特性。
簡介
在Ruby on Rails 系列的第 1 部分中,我們使用 Ruby on Rails 和 DB2 構建了一個 Team Room 示例,這個應用程序允許注冊的成員共享各種文本文檔、圖像文件和 XML 文檔。為了管理不斷增加的共享文檔集合,您學習了如何按類別對文檔分組。然後,學習了如何添加訂閱特性,這樣當用戶訂閱的文檔類別中添加了新文檔時,我們就可以通過電子郵件通知他們。在第一篇文章的末尾,成員能夠將不同類型的文件上載到 Team Room,文件被存儲在後端 DB2 數據服務器中。現在,我們進一步增強 Team Room,提供更高級的用戶特性和更好的資源訪問。
更新第 1 部分的 Team Room 應用程序以便進一步開發
步驟 1. 添加用戶管理特性
首先,在用戶模型和它的底層表持久存儲中增加一些必要的東西,從而支持正確的身份驗證:一個惟一的用戶 id 字符串和一個散列的密碼(使用 SHA 算法和一個偽隨機種子)。可以添加其他用戶屬性(比如有效性、等級等等)來進一步改進這個用戶模型,並對控制器和視圖做幾處修改來支持新用戶的注冊和安全登錄。我們的 Rails 項目放在 D:railsteamroom 目錄中,所以下面引用的所有路徑都是 D:railsteamroom 目錄中的相對路徑。
a) 執行ruby script/generate migration add_user_credentials_columns啟動遷移進程,在 USERS 表中添加必要的列。
b) 編輯 db/migrate/008_add_user_credentials_columns.rb 文件,添加必要的列(見清單 1):
清單 1. 編輯 008_add_user_credentials_columns.rb
class AddUserCredentialsColumns < ActiveRecord::Migration
def self.up
add_column :users, :userid, :string, :limit => 8
add_column :users, :hash_passwd, :string
add_column :users, :salt, :string
end
def self.down
remove_column :users, :userid
remove_column :users, :hash_passwd
remove_column :users, :salt
end
end
c) 運行rake db:migrate,在 USERS 表中添加這些新列。
步驟 2. 讓主題可供多個用戶訂閱
在第一篇文章描述的 Team Room 中,每個主題只能屬於一個訂閱。每個訂閱是一個用戶已經訂閱的主題集合,這讓我們的 Team Room 相當不真實,因為一個用戶選擇了某個主題之後,Team Room 中的其他用戶就不能再訂閱這個主題了。
隨著 Team Room 的流行,許多成員希望訂閱同一個主題。成員的這種要求是正常的,應該允許對同一主題進行多次訂閱。在更新的 Team Room 中,一個用戶可以通過一個訂閱訂閱許多主題。為了實現這一修改,需要執行以下步驟:
以前在 SUBSCRIPTIONS 和 SUBJECTS 之間存在一對多關系。但是,每個主題只能屬於一個訂閱。為了放寬這個限制,我們需要在 SUBSCRIPTIONS 和 SUBJECTS 之間建立多對多關系。
為了確保數據庫是規范化的(參見注 1),創建一個新的表 SUBJECTS_SUBSCRIPTIONS 來連接 SUBJECTS 和 SUBSCRIPTIONS 表。在中間連接表的名稱選擇方面,我們使用了 Ruby 約定:Active Record 假設這個連接表的名稱是兩個目標表名的組合(按字母表次序),它包含鏈接兩個目標表的外鍵對。
修改現有的關聯,並在 SUBJECTS_SUBSCRIPTIONS 表以及主題和訂閱模型中添加新的關聯來反映這些修改。
SUBJECTS_SUBSCRIPTIONS 表包含以下列:
表 1. SUBJECTS_SUBSCRIPTIONS 表列和描述
列名 數據類型 描述 SUBSCRIPTION_ID Integer SUBSCRIPTIONS 表的外部 ID SUBJECT_ID Integer SUBJECTS 表的外部 ID在遷移過程中,執行以下步驟:
a) 執行ruby script/generate migration create_subjects_subscriptions_table。
b) 編輯 db/migrate/009_create_subjects_subscriptions_table.rb 文件:
清單 2. 編輯 009_create_subjects_subscriptions_table.rb
class CreateSubjectsSubscriptions < ActiveRecord::Migration
def self.up
create_table :subjects_subscriptions, :id => false do |t|
t.column :subscription_id, :integer, :null => false
t.column :subject_id, :integer, :null => false
end
remove_column :subjects, :subscription_id
add_index :subjects_subscriptions, :subject_id
end
def self.down
drop_table :subjects_subscriptions
add_column :subjects, :subscription_id, :integer
end
end
c) 運行rake db:migrate創建 SUBJECTS_SUBSCRIPTIONS 表。
e) 將 /app/models/subject.rb 文件中現有的關聯belongs_to :subscription替換為新關聯has_and_belongs_to_many :subscriptions。
f) 將 /app/models/subscription.rb 文件中現有的關聯has_many: subject替換為has_and_belongs_to_many :subjects。
步驟 3. XML 數據:客戶信息
我們的市場營銷部門用 XML 格式收集了匿名的客戶信息,用來分析客戶的購物習慣。下面是零售商為市場研究收集的數據示例。
清單 3. XML 文檔示例
<marketinfo XMLns="http://www.ibm.com/developerworks">
<sales>
<customer>
<address>
<city>Nashville</city>
<state>TN</state>
<zip>46808</zip>
</address>
<categorIEs>
<category type='Toys'>
<item>
<SKU>2434901</SKU>
</item>
<item>
<SKU>9043272</SKU>
</item>
</category>
<category type='Video Games'>
<item>
<SKU>1915216</SKU>
</item>
</category>
</categorIEs>
<last_purchase>2007-05-12</last_purchase>
</customer>
</sales>
</marketinfo>
每個客戶購買記錄包含一個美國或加拿大的地址、產品類別細節(包括產品的 SKU 號)以及上一次購買的日期。產品類別包括:
Apparel
Automotive
Baby
Books
Computers
Cosmetics
Electronics
Garden & Patio
Home
Jewelry
MovIEs
Music
Pets
PharMacy
Sports
Toys
Video Games
後面一節演示如何使用 Team Room 應用程序對這個 XML 數據執行查詢操作。
可以將一份文本文檔格式的市場營銷報告與上面的 XML 客戶數據關聯起來。市場營銷報告可以包含市場分析結果或者關於數據收集方式的細節。
在第 1 部分中,我們創建了一個 XML 類型的列來存儲收集的市場營銷數據。為了高效地管理 XML 數據,像傳統的 SQL 數據類型一樣,DB2 在內部使用 XML 數據模型作為邏輯數據模型,同時也作為物理存儲的基本單元。另外,當指定 XML 數據類型時,會向數據庫用戶公開這個數據模型。盡管這為管理 XML 數據提供了強大的功能和靈活性,尤其適合以 XML 為中心的開發人員,但是當前對於能夠在 XML 列上執行的傳統數據庫管理活動類型還有一些限制。例如,包含 XML 列的表無法由 DB2 進行物理重組,這是因為 XML 具有層次化的存儲結構。這實際上意味著,在包含 XML 列的表中,不能用 ALTER 操作刪除列。盡管未來的 DB2 版本中可能會取消這個限制,但是現在要記住這個限制,因為這是 Ruby on Rails 遷移的一個基本操作。
為了繼續利用 Ruby on Rails 遷移的靈活性,同時保持 XML 的強大功能,我們要創建一個單獨的表來存儲 XML 數據。我們將這個表命名為 XML_CONTENTS。它將存儲 XML 文檔,DOCUMENTS 表仍然存儲其他所有相關信息。以後可以在 DOCUMENTS 表中添加或刪除列,而不會受到 XML_CONTENTS 表中 XML 數據的影響,也不會影響這些 XML 數據。
關於當前使用 XML 數據類型的限制的更多細節,請參考 IBM DB2 Database for Linux, UNIX, and Windows Information Center 中的 “Restrictions on native XML data store” 部分。
為了執行這個任務,我們生成並運行以下遷移:
a) 運行ruby script/generate migration create_xml_contents,這將創建 db/migrate/010_create_XML_contents.rb 文件。
b) 編輯 db/migrate/010_create_XML_contents.rb 文件:
清單 4. 編輯 010_create_XML_contents.rb
class CreateXMLContents < ActiveRecord::Migration
def self.up
drop_table :documents
create_table :documents do |t|
t.column :name, :string, :null => false
t.column :size, :integer, :null => false
t.column :data, :binary, :limit => 2.megabytes
t.column :content_type, :string, :null => false
t.column :created_at, :timestamp
t.column :updated_at, :timestamp
t.column :platform, :string, :limit =>10
t.column :subject_id, :integer
t.column :user_id, :integer
end
create_table :XML_contents do |t|
t.column :name, :string
t.column :data, :XML, :null => false
t.column :document_id, :integer
end
end
def self.down
drop_table :documents
drop_table :XML_contents
create_table :documents do |t|
t.column :name, :string, :null => false
t.column :size, :integer, :null => false
t.column :data, :binary, :limit => 2.megabytes
t.column :content_type, :string, :null => false
t.column :created_at, :timestamp
t.column :updated_at, :timestamp
t.column :platform, :string, :limit =>10
t.column :subject_id, :integer
t.column :user_id, :integer
t.column :xmldata, :XML, :null => false
end
end
end
c) 運行rake db:migrate以刪除現有的 DOCUMENTS 表、創建只存儲 XML 數據的新表 XML_CONTENTS 和新的 DOCUMENTS 表(不包含 XML 列)。
d) 在新的 DOCUMENTS 表和 XML_CONTENTS 表之間重新建立一個關系。
首先,在步驟 d 中生成的 /app/models/XML_content.rb 中添加belongs_to :document關聯。
第二,在 /app/models/document.rb 文件中添加has_one :XML_content關聯。
e) 上載功能與以前的文檔模型(document.rb,現在變成了父模型)中的實現相似,並添加創建子模型(XML_content)的操作。
NAME 列中存儲原來的文件名。
清單 5. 在 DOCUMENTS 記錄中指定文件屬性
self.name = File.basename(doc_fIEld.original_filename).gsub(/[^w._-]/, '')
self.content_type = doc_fIEld.content_type.chomp
self.size = doc_fIEld.size
self.created_at = Time.now
XML_CONTENTS.DATA 列存儲指定的文件。
清單 6. 向 XML_CONTENTS.DATA 指定 XML 文件內容
unless self.content_type.include?('text/XML')
self.data = doc_fIEld.read
else
content = XMLContent.new
content.name = self.name
content.data = doc_fIEld.read
self.XML_content = content
end
最終的 /app/models/document.rb 應該像清單 7這樣。
清單 7. document.rb
class Document < ActiveRecord::Base
belongs_to :user
belongs_to :subject
has_one :XML_content
# values displayed | stored
PLATFORM_TYPES = [ ['Neutral', 'Any'],
['Windows', 'WinXP'],
['Mac OS X', 'MacOS'],
['Linux', 'Linux']]
def uploaded_doc=(doc_fIEld)
self.name = File.basename(doc_fIEld.original_filename).gsub(/[^w._-]/, '')
self.content_type = doc_fIEld.content_type.chomp
self.size = doc_fIEld.size
self.created_at = Time.now
unless self.content_type.include?('text/XML')
self.data = doc_fIEld.read
else
content = XMLContent.new
content.name = self.name
content.data = doc_fIEld.read
self.XML_content = content
end
end
end
既然定義了 XML_CONTENTS 和 DOCUMENTS 表的模型,就必須修改視圖來處理上載和顯示功能。更新了上載功能和列出文檔的視圖,允許顯式地選擇要顯示的模型屬性(列)。
/app/vIEws/documents/list.rHtml 實現這種顯式的列選擇:
清單 8. /app/vIEws/documents/list.rHtml
<table cellpadding="0" cellspacing="0">
<tr>
<th>ID</th>
<th>Document name</th>
<th>Subject</th>
<th>Shared by</th>
<th>Size</th>
<th>Update at</th>
<th>Platform</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @documents.each_with_index do |document,i| %>
<% row_class = i%2 ==0 ? "even" : "odd" %>
<tr class="<%=row_class%>">
<td><%= document.id %></td>
<td><%= document.name %></td>
<% if document.subject %>
<td><%= link_to "#{document.subject.name}",
:controller => 'subjects', :action => 'list' %></td>
<% else %>
<td></td>
<% end %>
<% if document.user %>
<td><%= link_to "#{document.user.userid}",
:controller => 'users', :action => 'list' %></td>
<% else %>
<td></td>
<% end %>
<td><%= number_to_human_size( document.size ) %></td>
<td><%= document.updated_at.strftime("%d/%m/%Y %I:%M%p") %></td>
<td><%= document.platform. %></td>
<td><%= link_to 'Show', :action => 'show', :id => document %></td>
<td><%= link_to 'Edit', :action => 'edit', :id => document %></td>
<td><%= link_to 'Remove', { :action => 'destroy', :id => document },
:confirm => 'Are you sure?', :method => :post %></td>
</tr>
<% end %>
</table>
然後,對控制器做以下更新來處理 XML 數據的文檔顯示功能。這些代碼添加在 /app/controllers/documents_controller.rb 中。
清單 9. documents_controller.rb
def show
@document = Document.find(params[:id])
doc_type = @document.content_type
unless doc_type.include?('text/XML')
doc_content = @document.data
else
doc_content = @document.XML_content.data
end
send_data(doc_content,
:filename => @document.name,
:type => doc_type,
:disposition => "inline")
end
還要做以下更新來支持 XML 文檔上載,您可能注意到了主題和用戶關系處理的文檔實現。
清單 10. documents_controller.rb
def upload
if params[:document][:uploaded_doc].to_s.empty?
Flash[:notice] = "Please provide a file for upload"
redirect_to(:action => "new" )
else
@document = Document.new(params[:document])
@subject = params[:subject_name] && params[:subject_name].empty? ?
Subject.new :
Subject.find_by_name(params[:subject_name])
Document.transaction do
User.find(session[:user_id]).documents << @document
@subject.documents << @document
@subject.size = @subject.documents.size
if @subject.new_record?
@subject.name = params[:subject][:name]
@subject.tag = params[:subject][:tag]
@subject.description = params[:subject][:description]
@subject.save
end
if @document.save
Flash[:notice] = "Document #{@document.name} successfully created."
if @document.subject.subscriptions
SubscriptionMailer.deliver_notify(@document)
end
redirect_to :action => 'list'
else
render :action => 'new'
end
end
end
下面的圖 1描述了在執行上面的遷移步驟之後,Team Room 中不同模型之間的關聯。
圖 1. 修改之後不同模型之間新的關聯
將 XML 市場營銷數據上載到 Team Room
既然已經通過遷移實現了所有必需的模式修改,就可以將 XML 數據上載到 XML_CONTENTS 表中。將多個 XML 文檔插入表中最容易的方法是使用 DB2 9 數據服務器中的 IMPORT 實用程序。IMPORT 實用程序允許導入格式良好的 XML 文檔,可以同時進行 XML 模式檢驗,也可以不檢驗。對於我們當前的使用情況,需要確保正確地更新 XML_CONTENTS 表、DOCUMENTS 表和 SUBJECTS 表中的相關條目。因此,應該使用生成的 scaffold 每次上載一個文件,從而維護所有外鍵關聯。
在圖 2中可以看到,新文檔視圖/app/vIEws/documents/new.rHtml顯示創建了一個與 XML 內容相關聯的新文檔,還創建了與新文檔相關聯的新主題。
圖 2. 將 XML 文檔上載到 Team Room 存儲庫中
現在應該上載其他 XML 文檔,這些文檔包含加拿大和美國一些地區的模擬市場營銷數據,後面將用這些文檔運行 XQuery 和 XPath 搜索。這些 XML 文件放在 /test/fixtures 目錄中,這裡還放著用於文檔檢驗的 marketinfo.xsd XML 模式。在本文末尾的下載中可以找到更新後的 Team Room 應用程序。
XML 數據類型的基本 CRUD 操作
我們來執行一些基本的 XML 創建、獲取、更新和刪除(CRUD)操作,體會一下如何管理 XML 數據。
為了演示這些操作,假設市場營銷部門希望調查客戶生活的城市。在這些城市中將發起一次有針對性的市場營銷活動,從而提高品牌知名度和客戶光顧次數。為此,我們可以從 XML_CONTENTS 表中存儲的市場營銷信息 XML 文檔中提取出數據。另外,我們決定存儲產生的調查報告,以便記錄不斷增加的城市信息庫。如果決定以 XML 格式創建城市數據庫,那麼不需要創建另一個表來跟蹤這些數據。我們只需用這些數據組成一個 XML 文檔,然後將它作為 XML 插入同一列中。以後的某個時候,DBA 可能希望創建一個新的表來分隔應用程序數據,但這僅僅是一個邏輯和語義需求,不是數據庫的要求。
在 DB2 中,可以以多種方式查詢 XML 數據:使用 SQL、XQuery 或這兩者的組合。這個示例使用 XQuery。使用 XQuery 有助於用查詢結果構造 XML 文檔。清單 11給出要執行的 XQuery:
清單 11. 城市調查 XQuery
XQUERY
<citIEs>
declare default element namespace "http://www.ibm.com/developerworks";
{ for $c in fn:distinct-values(
db2-fn:XMLcolumn(
'XML_CONTENTS.DATA')/marketinfo/sales/customer/address/city)
order by $c
return <city>{$c}</city>
}
</citIEs>
首先看看函數db2-fn:xmlcolumn()中的語句,然後向外依次解釋,這樣才能理解這個 XQuery 的工作方式。db2-fn:XMLcolumn()函數從當前連接的 DB2 數據庫中的 XML 列獲取一個序列。在這裡,我們要從 XML_CONTENTS 表的 DATA 列獲取數據。但是,我們不需要所有數據,只需要 XPath 表達式指定的子集:/marketinfo/sales/customer/address/city。
換句話說,我們希望查看表中每一行中的所有 XML 文檔,並選擇 XPath 中出現的所有 city 元素。這會造成一個潛在的問題,因為多個客戶可能住在同一城市。為了處理這個問題,我們使用 XQuery 函數fn:distinct-values()。顧名思義,它將返回一系列不同的 city 元素,城市名不會重復。這個序列被賦值給變量$c。
在最後一步之前,對$c中的城市進行排序。然後返回結果。XQuery 的強大特性之一是,返回數據的格式是可以充分定制的。因為我們當前已經獲得了一系列城市名稱,所以將每個城市封裝在一個<city>元素中。這是一系列元素,還不是有效的 XML 文檔,因為它沒有根元素。為了確保返回有效的 XML 文檔,將整個結果放在一個<citIEs>元素中,所以數據現在像清單 12這樣(按照升序)。
清單 12. 返回的典型 XML 數據
<citIEs>
<city>Atlanta</city>
<city>Augusta</city>
<city>Austin</city>
<city>Baton Rouge</city>
<city> ... </city>
</citIEs>
要想了解 DB2 9 中的 XQuery 的更多信息,請參考 DB2 XML Guide(本文末尾的參考資料一節中提供了有關鏈接)。
現在已經獲得了 XML 文檔形式的城市列表,我們要將這個文檔插入數據庫中。開發人員可以按照與插入任何數據類型相同的方式將 XML 文檔插入數據庫中。
清單 13. 插入 XML 文檔
class DocumentsController < ApplicationController
[...]
def upload
[...]
@document = Document.new(params[:document])
@subject = params[:subject_name] && params[:subject_name].empty? ?
Subject.new :
Subject.find_by_name(params[:subject_name])
Document.transaction do
User.find(session[:user_id]).documents << @document
@subject.documents << @document
@subject.size = @subject.documents.size
if @subject.new_record?
@subject.name = params[:subject][:name]
@subject.tag = params[:subject][:tag]
@subject.description = params[:subject][:description]
@subject.save
end
if @document.save
Flash[:notice] = "Document #{@document.name} successfully created."
[...]
end
因為在任何給定的時刻客戶的數量都不是靜態的,所以我們希望定期更新客戶城市的列表,比如每周一次或每月一次,從而保證這個列表反映最新情況。為此,可以執行相同的 XQuery 語句。數據庫中當前的文檔會被替換為 XML 文件的最新版本,因為它的 id 是相同的。注意,有一個傳遞給編輯視圖表單(/app/vIEws/documents/_form.rHtml)的隱藏的 :id 參數,在 DocumentsController 中使用這個參數獲取現有的文檔:
清單 14. 更新 XML 文檔
class DocumentsController < ApplicationController
[...]
def update
@document = Document.find(params[:document][:id])
if @document.update_attributes(params[:document])
Flash[:notice] = 'Document was successfully updated.'
redirect_to :action => 'show', :id => @document
else
render :action => 'edit'
end
end
[...]
end
在內部,DB2 重新分配現有的 XML 數據頁面並插入新的值。所以實際效果就是用新的更新後的文檔替換整個 XML 文檔。
最後,當數據不再有用時,或者數據可以由其他機制處理時,就需要刪除數據。例如,DBA 可能希望把不同的信息隔離開,比如使用另一個表中的另一個 XML 列。為了刪除文檔,需要執行 DELETE 語句,並加上適當的 WHERE 謂詞。在 Ruby 中,可以這樣做:
清單 15. 刪除 XML 文檔
class DocumentsController < ApplicationController
[...]
def destroy
Document.find(params[:id]).destroy
redirect_to :action => 'list'
end
end
查詢 XML 數據
除了基本的 CRUD 操作之外,還可以利用 XQuery 語言的功能非常精確地控制如何執行查詢以及如何返回數據。例如,盡管獲得客戶所在的城市的列表是有意義的,但是市場營銷部門更希望知道哪些類別的產品最暢銷。這可以使市場營銷活動更有針對性,不必將市場營銷活動的預算分散在所有產品上。度量產品是否暢銷的一種方法是,檢查最近的銷售是否是在某一產品類別中發生的,或者在特定的時間段內這些類別的產品是否暢銷。
為了進行數據挖掘,我們首先問一個問題:在 2007 年 4 月 15 日到 2007 年 4 月 30 日之間,客戶購買了哪些類別的產品?
為了回答市場營銷部門的這個問題,使用清單 16所示的查詢:
清單 16. 獲得 4 月 15 日到 4 月 30 日之間的產品類別銷售的 XQuery
XQUERY
declare default element namespace "http://www.ibm.com/developerworks";
<categorIEs>
{ let $categorIEs := fn:distinct-values(
for $c in db2-fn:xmlcolumn( "XML_CONTENTS.DATA")/marketinfo/sales/customer
where xs:date($c/last_purchase) > xs:date("2007-04-15")
and xs:date($c/last_purchase) <= xs:date("2007-04-30")
return $c/categorIEs/category/@type)
for $c in $categorIEs
return <category>{$c}</category>
}
</categorIEs>
與前面的示例相似,我們要返回一個有效的 XML 文檔,所以將 XQuery 的結果封裝在 XML 根元素<categories>中。這個 XQuery 分為兩個部分。首先,一個 “let” 語句將一個表達式與變量$categorIEs綁定在一起。然後,將這個變量用作for語句的上下文。我們來分別分析這些語句。
“let” 語句本身由一個 FLWOR(for、let、where、order by 和 return) XQuery 表達式組成。您可能會注意到,FLWOR 表達式常常相當於 SQL 中的 SELECT-FROM-WHERE 塊。然後,循環遍歷每個客戶:for $c in db2-fn:xmlcolumn("XML_CONTENTS.DATA") /marketinfo/sales/customer,條件是購買日期晚於 2007 年 4 月 15 日,並早於 2007 年 4 月 30 日。如果購買滿足這個條件,市場營銷部門就想知道這次購買的情況:where xs:date($c/last_purchase) > xs:date("2007-04-15") and xs:date($c/last_purchase) <= xs:date("2007-04-30")。
對於這些購買,我們返回存儲為 XML 屬性的類別類型:$c/categories/category/@type。與前一個示例一樣,使用fn:distinct-values()函數避免重復出現同一類別中的購買。將這個類別列表綁定到變量$categorIEs。
現在,$categorIEs存儲著一系列不重復的類別,但是因為我們希望返回 XML,所以將序列中的每個值封裝在<category>XML 元素中。最終的輸出與清單 17相似,輸出執行了過濾操作,只包含 4 月 15 日到 4 月 30 日之間的購買。
清單 17. 4 月 15 日到 4 月 30 日之間發生購買的產品類別的數據示例
<categorIEs>
<category>Home</category>
<category>Electronics</category>
<category>Apparel</category>
<category>Gifts & Flowers</category>
<category>Baby</category>
</categorIEs>
這個 XQuery 可以進行參數化,這樣就能夠在市場營銷部門感興趣的任何時間范圍上執行查詢。另外,為了封裝這個查詢,可以將該功能放在一個存儲過程中,這樣就可以為 Rails 開發人員提取 XQuery 細節。
在下面的圖 3中,文檔列表視圖(/app/vIEws/documents/list.rHtml)布局還包含預構建的報告表單,可以用來執行 XQuery 和 XPath 搜索。
圖 3. Team Room 文檔存儲庫和預構建的市場營銷報告
進一步查詢 XML 數據
既然已經嘗到了甜頭兒,市場營銷部門對查詢數據的要求就越來越高了。他們想知道在一個特定地區(具有特定的郵政編碼)某類產品售出了多少。郵政編碼代表特定區域,該區域中可能售出了大量產品,或者有大量客戶投訴。這種信息可以幫助市場營銷人員和產品經理判斷哪些產品在哪些銷售區域暢銷,並將產品與郵政編碼相關聯。市場營銷部門可以考慮在一個區域中哪些產品應該停止銷售,根據某地區的產品暢銷度確定市場營銷重點。
我們將使用 SQL/XML 函數 XMLQUERY() 實現這個查詢。用 SQL/XML 進行查詢可以結合這兩種技術的長處。例如,可以使用 SQL/XML 執行以下操作:
對關系數據和 XML 數據使用謂詞
訪問和提取 XML 數據片段
在 SQL 級對 XML 數據進行聚合和分組
連接關系數據和 XML 數據
將參數傳遞給 XQuery 表達式
我們將在 XQuery 中使用 (1)、(2) 和 (5) 項操作。為了簡單,我們不對示例進行參數化,並假設我們感興趣的是 “Jewelry” 產品類別在郵政編碼為 “79081” 的地區的銷售情況。但是,本文提供的應用程序代碼(見本文末尾的下載一節)包含的查詢允許任何類別或郵政編碼的參數。
清單 18. 獲取特定郵政編碼地區中特定產品類別總銷售量的 SQL/XML 查詢
select name, XMLquery(
'declare default element namespace "http://www.ibm.com/developerworks";
let $total := sum (
for $i in $t//category
let $sum := count($i/item)
where $i/@type = "Jewelry"
return $sum
)
return <total>{$total}</total>'
passing data as "t"
) as data
from teamroom.documents
where XMLexists(
'declare default element namespace "http://www.ibm.com/developerworks";
$t/marketinfo/sales/customer/address[zip = "79081"]'
passing data as "t"
)
這個 SELECT 語句對於每個地區返回一行,地區名由name列表示。另外,對於每一行,返回一個有效的 XML 文檔,其中只包含一個根元素<total>。因為我們要在 SQL 語言中調用 XQuery,所以必須使用 SQL/XML 函數XMLQUERY()並讓 XQuery 引擎知道操作所針對的上下文。在前面的示例中,這是使用db2-fn:xmlcolumn()函數執行的。在這裡,我們使用子句passing data as “t”。我們指定data是要操作的 XML 列,並將它賦值給變量t。在 XQuery 中每個出現變量$t的地方,DB2 將它替換成 SELECT 語句當前行的data列中的 XML 文檔。
在 SQL 級上執行的另一項處理是,我們不希望XMLQUERY()對列中的所有 XML 文檔執行操作。我們希望將查詢的范圍限制在包含郵政編碼為 “79081” 的客戶的行。找到這些行之後,只對它們執行 XQuery。也就是查詢這些行中的所有客戶購買記錄,無論其郵政編碼是否為 “79081”。
這個過濾處理是在 SQL WHERE 子句中用 SQL/XMLXMLEXISTS()函數謂詞實現的。XMLEXISTS謂詞判斷一個 XQuery 表達式是否返回一個元素序列。在我們的示例中。傳遞給XMLEXISTS()的 XQuery 表達式是一個簡單的 XPath 表達式,其中的$t是 XML 列data:$t/marketinfo/sales/customer/address[zip = "79081"]。
這個 XPath 表達式可以解釋為:返回郵政編碼為 “79081” 的 address 元素的序列。如果指定的 XPath 返回一個空序列,那麼XMLEXISTS返回 false。否則返回 true。既然 SQL 已經幫助我們獲得了 XML 文檔的子集,XQuery 的操作范圍就確定了;XQuery 使用一個綁定到變量$total的let。$total被賦值為一個 FLWOR 表達式的結果。對於類別類型為 “Jewelry”(where $i/@type = "Jewelry")的 XML 文檔中的任何<category>元素(for $i in $t//category),計算銷售量並賦值給$sum(let $sum := count($i/item)),最後返回sum(return $sum)。
這返回每個客戶的銷售額。將結果傳遞給sum()函數,從而求出某個類別的所有客戶的總銷售額。現在獲得了$total。因為我們希望返回 XML,所以將總銷售額封裝在<total>元素中(return <total>{$total}</total>')。最後的結果類似於return <total>198</total>'。
要想了解在 DB2 9 中使用 XQuery 的更多信息,請參考 DB2 XML Guide(參考資料一節中提供了這個文檔的鏈接)。
分解 XML 數據
請考慮一個場景:一個新構建的應用程序需要與不支持本地 XML 的遺留應用程序和存儲庫集成。XML 文檔中存儲的信息可能需要放在一個關系表對象中。
假設市場分析的結果需要存儲在關系表中,供一個遺留應用程序進行分析。這就要求分解 XML 數據並將它插入關系表中。
pureXML 提供了許多直接操作 XML 數據的工具。如果需要的話,pureXML 也能夠執行分解。DB2 9 數據服務器為執行 XML 分解提供了以下內置工具。
1. XDBDECOMPXML 存儲過程
可以使用 DB2 9 中的帶注釋的 XML 模式分解特性將 XML 文檔分解到關系表。顧名思義,這個特性在 XML 模式中使用注釋作為映射語言,將 XML 文檔中的信息映射到關系表。它需要一種 XML 模式,這個 XML 模式文檔必須存儲在 DB2 XSR 中並表明用於分解。可以通過一個 DB2 存儲過程(xdbDecompXML)調用將 XML 文檔分解到映射的關系列,也可以使用一個 Command Line Processor(CLP)命令。
關於通過 Visual Studio Add-in 執行 XML 分解的細節,請參考 “Introduction to annotated XML schema decomposition using the DB2 Visual Studio 2005 Add-in”。
注釋 XML 模式的另一種方法是使用 DB2 Developer Workbench(DWB)。DWB 可以免費下載,它為創建、編輯、調試、部署和測試 DB2 數據庫應用程序提供了一個完整的環境,包括開發存儲過程和用戶定義的函數。在本文末尾的參考資料一節中提供了下載信息。
DWB 的組件之一是用於帶注釋的 XML 模式分解的 Mapping Editor。可以使用這個簡單直觀的圖形界面在 XML 模式和關系模式之間建立映射。以圖形化方式將 XML 元素或屬性映射到 DB2 中的關系列,就會自動地在 XML 模式文檔中加上注釋。保存 XML 模式並在 XSR 中注冊之後,就可以將 XML 文檔分解到 DB2 數據庫中。
對 XML 模式注釋和 xdbDecompXML 存儲過程的全面討論超出了本文的范圍。要想了解關於帶注釋的 XML 模式分解的更多信息,包括基於內容的有條件分解或指定在插入前應用的內容轉換等高級特性,請參考 DB2 9 XML Guide(參見本文的參考資料一節)。要想進一步了解 XML Extender 以及它的分解方法,請參考 Mayank Pradhan 撰寫的 developerWorks 文章 “From DAD to Annotated XML Schema Decomposition”。
2. XMLTABLE SQL 表函數
XMLTABLE 是一個 SQL 表函數,它對一個 XML 文檔執行 XQuery 表達式並返回一個表。返回的表可以包含任何 SQL 數據類型的列,包括 XML。通過結合使用 XMLTABLE 和 INSERT 語句(這常常被稱為 “Insert-from-XMLTABLE” 語句),可以將從 XML 文檔獲取的值插入關系表中。這樣就可以實現與帶注釋的 XML 模式分解相同的功能。
這提供了一種執行 XML 分解的簡單方法,可以將 XML 文檔的片段存儲在關系表的各個列中。
下面的語句執行一個 XQuery 表達式,並以表的形式返回 DATA 列中的值。每一行顯示與特定客戶相關聯的城市、州、郵政編碼和最後購買日期。
清單 19. XMLTABLE 函數
SELECT X.CITY, X.STATE, X.ZIP, X.LAST_PURCHASE FROM
TEAMROOM.XML_CONTENTS,
XMLTABLE (XMLNAMESPACES (DEFAULT 'http://www.ibm.com/developerworks'),
'db2-fn:xmlcolumn("XML_CONTENTS.DATA")//customer'
COLUMNS
"CITY" CHAR(16) PATH './address/city',
"STATE" CHAR(16) PATH './address/state',
"ZIP" CHAR(6) PATH './address/zip',
"LAST_PURCHASE" DATE PATH './last_purchase') as X
以上 XQuery 的輸出類似下面所示:
清單 20. XMLTABLE 輸出示例
Baton Rouge LA 77888 03/10/2007
Baton Rouge LA 14257 01/07/2007
Richmond VA 78045 01/26/2007
Oklahoma City OK 71107 04/13/2007
Tallahassee FL 41720 04/25/2007
Richmond VA 39591 03/25/2007
Richmond VA 36522 03/23/2007
Richmond VA 32230 02/12/2007
Charleston WV 33015 02/12/2007
Columbia SC 72647 01/11/2007
Raleigh NC 11238 04/02/2007
Nashville TN 21245 01/06/2007
Fankfort KY 53793 04/18/2007
Austin TX 35462 03/13/2007
Columbia SC 68359 01/01/2007
Jackson MS 25770 01/20/2007
Little Rock AR 46342 03/10/2007
Tallahassee FL 54306 01/20/2007
Charleston WV 44339 02/20/2007
Frankfort KY 92403 02/27/2007
<etc ........>
假設定義了具有正確定義的 SQL 類型的 CUSTOMER_INFOS 表,就可以用一個 INSERT 語句封裝 SELECT 語句,從而將 XML 市場營銷信息中提取出的客戶數據(表格式)插入關系表:
清單 21. 將分解的 XML 數據插入關系表
INSERT INTO TEAMROOM.CUSTOMER_INFOS
SELECT X.CITY, X.STATE, X.ZIP, X.LAST_PURCHASE FROM
TEAMROOM.XML_CONTENTS,
XMLTABLE (XMLNAMESPACES (DEFAULT 'http://www.ibm.com/developerworks'),
'db2-fn:xmlcolumn("XML_CONTENTS.DATA")//customer'
COLUMNS
"CITY" VARCHAR(16) PATH './address/city',
"STATE" CHAR(16) PATH './address/state',
"ZIP" CHAR(6) PATH './address/zip',
"LAST_PURCHASE" DATE PATH './last_purchase') as X
另外,Ruby 提供了一個稱為 Ruby Electric XML(REXML)的 XML 處理器。可以使用它對 XML 數據進行樹解析和流解析。
Web 2.0 時代的新需求
本系列的第 1 部分引入了訂閱特性,讓用戶能夠訂閱感興趣的主題。當訂閱的主題出現更新時,用戶會收到電子郵件通知。多麼古老的方式,似乎還停留在 20 世紀。在這個 Web 2.0 時代,一些用戶可能希望在閱讀器中以 RSS 或 Atom 提要形式接收這些通知。
可以使用 DB2 9 pureXML 生成這樣的提要,用戶可以在提要閱讀器中方便地查看這些。我們使用 SQL/XML XML 發布函數來實現這個特性。XML 發布函數用來構造 XML 節點和文檔。關系數據和 XML 數據都是可以使用的。
注:發布函數有時候也稱為構造器函數。
DB2 9 中的 XML 發布函數包括:
XMLNAMESPACES
XMLELEMENT
XMLATTRIBUTE
XMLFOREST
XMLDOCUMENT
XMLCONCAT
XMLCOMMENT
XMLPI
XMLTEXT
我們來提供一個 Atom 提要,當訂閱的主題發生更新時,它會通知用戶。盡管我們選用 Atom 提要,但是 DB2 同樣能夠生成 RSS 提要。這僅僅是選用哪種提要 XML 模式來構造提要的問題。
清單 22. Atom 提要存儲過程
CREATE PROCEDURE GET_ATOM_FEED ( )
DYNAMIC RESULT SETS 1
------------------------------------------------------------------------
-- SQL Stored Procedure
------------------------------------------------------------------------
P1: BEGIN
-- Declare cursor
DECLARE cursor1 CURSOR WITH RETURN FOR
SELECT XMLSERIALIZE(
XMLDOCUMENT(
XMLELEMENT (NAME "feed",
XMLNAMESPACES(DEFAULT 'http://www.w3.org/2005/Atom'),
XMLCONCAT (
XMLELEMENT (NAME "id", 'http://localhost:3000/documents'),
XMLELEMENT (NAME "title", 'Teamroom Documents'),
XMLELEMENT (NAME "updated", CURRENT TIMESTAMP),
XMLELEMENT (NAME "link",
XMLATTRIBUTES('http://localhost:3000/documents/atom_feed'
as "href", 'self' as "rel")),
XMLELEMENT (NAME "author",
XMLCONCAT(
XMLELEMENT (NAME "name", 'TeamRoom'),
XMLELEMENT (NAME "email", 'teamroom@developerWorks.ibm.com')
)
),
XMLAGG (
XMLELEMENT (NAME "entry",
XMLCONCAT (
XMLELEMENT (NAME "title", name),
XMLELEMENT (NAME "id", 'http://localhost:3000/documents/show/'
|| CHAR(id)),
XMLELEMENT (NAME "updated", updated_at),
XMLELEMENT (NAME "link", 'http://localhost:3000/documents/show/'
|| CHAR(id)),
XMLELEMENT (NAME "category", category),
XMLELEMENT (NAME "summary", content_type),
XMLELEMENT (NAME "content", XMLATTRIBUTES('text' as "type"), content)
)
)
)
)
)
)
AS CLOB INCLUDING XMLDECLARATION
)
FROM (SELECT d.id as id, d.name as name, d.content_type as content_type,
d.updated_at as updated_at, s.name as category, s.description as content
FROM DOCUMENTS d, SUBJECTS s
WHERE d.subject_id = s.id
ORDER BY d.updated_at DESC
FETCH FIRST 10 ROWS ONLY)
AS doc_list;
-- Cursor left open for clIEnt application
OPEN cursor1;
END P1
我們選擇以存儲過程的形式提供這個功能,這樣 Rails 開發人員就不需要為生成 Atom 提要的細節操心。開發人員只需調用這個存儲過程即可。
清單 23. 調用 Atom 提要存儲過程
class Document < ActiveRecord::Base
[...]
def atom_feed
feed = Document.find_by_sql("call teamroom.get_atom_feed()")
content = feed[0].attributes["1"]
send_data(content,
:filename => 'TeamRoomFeed.atom',
:type => 'text/XML',
:disposition => "inline")
end
[...]
end
對 DB2 XML 發布函數和 Atom Syndication Format Protocol 的全面討論超出了本文的范圍。更多的信息請參考參考資料一節中的鏈接。另外,本文提供的應用程序下載包包含 Atom 提要的完整實現,包括用來生成 feed 的存儲過程和應用程序代碼。
結束語
DB2 的 pureXML 功能可以將 XML 數據存儲為其固有的層次化格式,這使應用程序能夠輕松地使用 XML 語言,同時保持 DB2 關系數據庫管理系統提供的性能、可伸縮性、可靠性和可用性優勢。再加上 Ruby on Rails 框架的簡單性和靈活性,開發人員就可以快速輕松地構建、部署和維護高質量的 Web 2.0 應用程序。這兩種技術的組合非常靈活,可以根據業務的需要將信息從關系格式轉換為層次化格式。在轉換數據格式的同時,仍然能夠保持出色的性能(因為使用了同樣的優化技術),而且可以應用 DB2 提供的所有其他可靠性和可用性特性:比如備份、恢復和可伸縮性。
DB2 pureXML 不僅支持基本的本地 XML 支持,還可以以節點級粒度在硬盤上存儲解析後的數據,從而支持通過編制索引(實際文檔節點位置)來提高查詢性能,以及使用 DB2 查詢引擎中內置的 XQuery 和 XPath 原語。這使它非常適合在 Ruby on Rails 運行時中使用,因為 Ruby on Rails 提供了一組非常出色的 XML 庫(例如 REXML 和 ROXML),可以快速地將 XML 數據存儲與 Web 應用程序集成起來。