DB2 的 pureXML 特性与 Ruby on Rails

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:\rails\teamroom 目录中,所以下面引用的所有路径都是 D:\rails\teamroom 目录中的相对路径。

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_IDIntegerSUBSCRIPTIONS 表的外部 ID
SUBJECT_IDIntegerSUBJECTS 表的外部 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 文档示例
                
NashvilleTN46808
2434901904327219152162007-05-12

每个客户购买记录包含一个美国或加拿大的地址、产品类别细节(包括产品的 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
                
IDDocument nameSubjectShared bySizeUpdate atPlatform
'subjects', :action => 'list' %> 'users', :action => 'list' %> 'show', :id => document %> 'edit', :id => document %> 'destroy', :id => document }, :confirm => 'Are you sure?', :method => :post %>

然后,对控制器做以下更新来处理 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 文档

现在应该上载其他 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

   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 {$c}
   }

首先看看函数 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 的强大特性之一是,返回数据的格式是可以充分定制的。因为我们当前已经获得了一系列城市名称,所以将每个城市封装在一个 元素中。这是一系列元素,还不是有效的 XML 文档,因为它没有根元素。为了确保返回有效的 XML 文档,将整个结果放在一个 元素中,所以数据现在像清单 12 这样(按照升序)。


清单 12. 返回的典型 XML 数据
                
AtlantaAugustaAustinBaton Rouge ... 

要想了解 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";

  { 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 {$c}
  }

与前面的示例相似,我们要返回一个有效的 XML 文档,所以将 XQuery 的结果封装在 XML 根元素 中。这个 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,所以将序列中的每个值封装在 XML 元素中。最终的输出与清单 17 相似,输出执行了过滤操作,只包含 4 月 15 日到 4 月 30 日之间的购买。


清单 17. 4 月 15 日到 4 月 30 日之间发生购买的产品类别的数据示例
                
HomeElectronicsApparelGifts & FlowersBaby

这个 XQuery 可以进行参数化,这样就能够在市场营销部门感兴趣的任何时间范围上执行查询。另外,为了封装这个查询,可以将该功能放在一个存储过程中,这样就可以为 Rails 开发人员提取 XQuery 细节。

在下面的图 3 中,文档列表视图(/app/views/documents/list.rhtml)布局还包含预构建的报告表单,可以用来执行 XQuery 和 XPath 搜索。


图 3. Team Room 文档存储库和预构建的市场营销报告
Team Room 文档存储库

进一步查询 XML 数据

既然已经尝到了甜头儿,市场营销部门对查询数据的要求就越来越高了。他们想知道在一个特定地区(具有特定的邮政编码)某类产品售出了多少。邮政编码代表特定区域,该区域中可能售出了大量产品,或者有大量客户投诉。这种信息可以帮助市场营销人员和产品经理判断哪些产品在哪些销售区域畅销,并将产品与邮政编码相关联。市场营销部门可以考虑在一个区域中哪些产品应该停止销售,根据某地区的产品畅销度确定市场营销重点。

我们将使用 SQL/XML 函数 XMLQUERY() 实现这个查询。用 SQL/XML 进行查询可以结合这两种技术的长处。例如,可以使用 SQL/XML 执行以下操作:

  1. 对关系数据和 XML 数据使用谓词
  2. 访问和提取 XML 数据片段
  3. 在 SQL 级对 XML 数据进行聚合和分组
  4. 连接关系数据和 XML 数据
  5. 将参数传递给 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}'
                   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 文档,其中只包含一个根元素 。因为我们要在 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/XML XMLEXISTS() 函数谓词实现的。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 使用一个绑定到变量 $totallet$total 被赋值为一个 FLWOR 表达式的结果。对于类别类型为 “Jewelry”(where $i/@type = "Jewelry")的 XML 文档中的任何 元素(for $i in $t//category),计算销售量并赋值给 $sumlet $sum := count($i/item)),最后返回 sumreturn $sum)。

这返回每个客户的销售额。将结果传递给 sum() 函数,从而求出某个类别的所有客户的总销售额。现在获得了 $total。因为我们希望返回 XML,所以将总销售额封装在 元素中(return {$total}')。最后的结果类似于 return 198'

要想了解在 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


假设定义了具有正确定义的 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 应用程序集成起来。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/9403012/viewspace-89/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/9403012/viewspace-89/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值