开发人员:Ruby on Rails
   下载
 Oracle 数据库快捷版
 Ruby OCI8 Adapter
 Ruby on Rails
 Ruby Gems
 
   标签
xe, rubyonrails, 全部
 

 

轻松集成:准确地从 XML 到数据存储


作者:Matt Kern

使用 ActiveRecord 和 XML::Mapping 实现 XML 数据在 Oracle 数据库中的简单持久性。

2007 年 6 月发表

XML 已经成为世界通用的数据交换格式,而 Ruby on Rails 是该框架的完全参与者。通过组合使用 XML::Mapping Ruby gem 和 Rails 的 ActiveRecord 组件(无需任何其他重要组件),您可以用比想像中更少的代码来解析 XML 文档,将其映射到对象,操纵该对象,并将其保存到 Oracle 数据库后端。作为附带的好处,在使用 Rails 堆栈提供的服务时,您可以拥有非凡的 ActiveRecord 所具有的全部功能和灵活性。

在 Ruby 的世界中,有几个选择可用于从 XML 向对象和后端列集和散集数据。有两个选择可以很容易地使值与 Ruby 对象互为映射,而 ROXML 是其中较为简单的一个。然而,ROXML 的简单性同时也是它最大的弱点。ROXML API 不像 XML::Mapping 的 API 那样丰富。举例来说,您无法在 ROXML 中为给定元素或属性指定默认值,但 XML::Mapping 中有一个 API 就可以做到这一点。这两个库都是由 gem 安装的,因此安装都很简单。这两个库还依赖于 REXML 进行 XML 解析,尽管 XML::Mapping 有其自己的 XPath 实现。

虽然使用这两个库的任何一个实际上都可以得到同样的结果(只是在实现上稍有不同,但原理是相同的),在本文中,我还是选择使用 XML::Mapping gem,因为其具有更完备的 API。这两个库都要求您将其“包括”进来,因为它们都是作为 Ruby 模块实现的。(Ruby 模块使您可以通过“include”将模块方法“混合”到一个类中。通过这种方式,可以实现许多类的公共功能,就好像被 Enumerable 模块缩影了一样。)

在这两种情况下,库与 ActiveRecord 的配合都不好。ActiveRecord 大量使用了类似 method_missing 的 hook — 大家时而亲切时而不亲切地称之为“Railsy magic”。这意味着,该实现并不像直接将 XML::Mapping 模块包含在 ActiveRecord 类中一样简单。稍后您会明白这意味着什么,现在我们首先要安装好所需的组件。

安装


首先安装 XML Mapping gem。与所有 gem 一样,安装过程很简单。只需发出以下命令:
    
$ sudo gem install xml-mapping
安装 gem 时,您可能需要根用户权限;除非您已经是根用户了,否则必须使用 sudo 关键字。现在,我们检查一下安装是否正确:
$ gem list xml-mapping --local

*** LOCAL GEMS ***

xml-mapping (0.8.1)
An easy to use, extensible library for mapping Ruby objects to XML
and back.Includes an XPath interpreter.

如果当前开发的并非 Rails 应用程序,但您想同时使用 XML::Mapping gem 和 ActiveRecord 的丰富功能,则同样可以实现。ActiveRecord 可用于 Rails 框架之外,使用它可以在开发数据转储自动化脚本或其他应用程序时节省大量的时间。要在 Rails 之外使用 ActiveRecord,您需要安装 ActiveRecord gem,安装使用的命令与安装其他任何 gem 的命令相同:

      
$ sudo gem install activerecord --include-dependencies     

上面的 install 命令可同时安装 ActiveRecord 及其相关 ActiveSupport。既然安装了 ActiveSupport,可以浏览一下有关的 API 文档。ActiveSupport 提供了对 Ruby 内核的一些极好的扩展,可以使一些代码(如计算日期等)更加简单易读。同样,检查以确保安装正确:

      
$ gem list activerecord --local

*** LOCAL GEMS ***

activerecord (1.15.2)
Implements the ActiveRecord pattern for ORM.    
如果当前开发的是一个 Rails 应用程序,则 Rails 应该已经安装好了。如果已经安装了 Rails,则已经拥有了 ActiveRecord,这时可以忽略上述 activerecord gem 的安装过程。如果您一直跟随着本系列文章,应该已经准备好了。如果您对所有这些是个新手,可以看一下 Oracle 上的 Ruby on Rails:一个简单的教程

相关库

我在前面提到过,这两个 XML 映射库都依赖于 REXML。但我们不需要安装 REXML,因为它是 Ruby 内核的一部分。REXML 有一个速度较慢的名声,但是如果使用 REXML 及其后代能够快速地编写一个测试程序,那么最好可以花些时间来了解它如何很好地为我们服务。

REXML 已经成为了 Ruby 下的 XML 解析的实际标准,但最近 libxml 项目大有赶超之势。根据 libxml 项目网站的非正式标准,libxml 比 REXML 快得多,特别是对于 XPath。在某点上,这两个库中的一个或两个将有望与 libxml 项目兼容。但是,请记住,XML::Mapping gem 包含有自己的 XPath 实现,并将一些极好的特性(如预编译 XPath 查询和写访问)添加到 XPath 表达式中。我之所以提到 REXML 的 XPath 实现速度较慢,是因为:尽管 XML::Mapping gem 带有自己的 XPath 实现,有时仍使用 EXML 的 XPath 实现。通过使用 Ruby 的监测器,您可以观察在使用 gem 时会进行哪些调用,您会看到至少在使用 XML::Mapping#load_from_xml 时调用了 REXML XPath 解析器。

并非太难的细节

做好以上准备后,我们来看一些代码。只要设置 ActiveRecord,使其有一个到 Oracle 数据库的有效连接,本例中的大多数代码就可以在 Rails 环境之外运行。要使 ActiveRecord 在 Rails 环境之外工作,只需在您的脚本或非 Rails 应用程序中 require 它并建立一个连接即可。下面就是一个例子:
     
require 'rubygems'
require_gem 'activerecord'

ActiveRecord::Base.establish_connection( :adapter => "oci", :host => "localhost/XE", 
:username => "discographr", 
:password => "password" )

Class Person < ActiveRecord::Base
end
如前所述,ActiveRecord 和 XML::Mapping 可使脚本的编写简单而快速。假设我们需要使用一个脚本将数据从数据库转储导出到 XML,就会体会到上述好处了。阅读完本文后,您将对这一点有更深的体会!

如果您不知道如何使 Rails 应用程序与 Oracle 对话,请参阅 Obie Fernandez 的“Connecting to Oracle from Ruby on Rails”

 
<?xml version="1.0" encoding="UTF-8" ?>
<person>
<name>Ruby Jones</name>
<age>45</age>
<address name="home">
<street_address>1200 Main Street</street_address>
<city>Anytown</city>
<state>SD</state>
<zip_code>12345</zip_code>
</address>
<address name="work">
<street_address>9898 Center Street</street_address>
<city>Anotherville</city>
<state>SD</state>
<zip_code>11223</zip_code>
</address>
</person>
您需要为 ActiveRecord 创建一个模式,以用于 Person 模型:

     
CREATE TABLE people (
id NUMBER(38) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age NUMBER(3)
)

CREATE TABLE addresses (
id NUMBER(38) PRIMARY KEY,
street_address VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
state VARCHAR(2) NOT NULL,
zip_code NUMBER(5) NOT NULL,
name VARCHAR(100) NOT NULL,
person_id NUMBER(38)
)         
准备好模式之后,下一步是创建 ActiveRecord 类:
     
class Person < ActiveRecord::Base
has_many :addresses
end

class Address < ActiveRecord::Base
has_one :person
end       
请记住,ActiveRecord 子类会查找一个名为“people”(“people”的复数形式)的表,然后从表中推断出为其指定类的属性。因此,如果已有一个到数据库的有效连接,即使您从未在模型类中显式地定义 :name 或 :age,仍然可以进行以下调用:
     
user = Person.new ( :name => "Chuck Palahniuk",
:age => 45 )
user.save!        
前面提到过,ActiveRecord 和 XML 映射库在一起配合得不好。现在的情况是,它们不能共享!理想情况下,您只需将 XML::Mapping 库包含在 ActiveRecord 子类中,它们即可在一起协调工作,而您可以直接从 XML 文档中加载模型的属性。或者,您也可以从数据库加载一个对象,并将该对象写入 XML。但实际情况并非如此简单。(尽管经过一些代码重写这两个库可一起工作了!)在涉及属性访问器时,它们互相干扰。由于这一原因,不要添加从 XML::Mapping 到 ActiveRecord 子类的 include 语句,而要为模型的 XML 表示层创建 wrapper 类:

require_gem 'xml-mapping'

class AddressXml
include XML::Mapping
  
text_node :street_address, "street_address"
text_node :city, "city"
text_node :state, "state"
numerical_node :zip_code, "zip_code"
text_node :name, "@name"
end


class PersonXml
include XML::Mapping

text_node :name, "name"
hash_node :address, "address", "@name", :class => AddressXml
end
在定义这些类时,它们的顺序是很重要的。由于 PersonXml 会在调用 hash_node 时引用 AddressXml,我们应该在 PersonXml 之前定义 AddressXml。如果没有首先定义 AddressXml,hash_node 调用就会抱怨说 AddressXml 尚未定义。颠倒定义的顺序,使用 forward declaration,这样做也是可行的:
require_gem 'xml-mapping'

# forward declaration
class AddressXml; end

class PersonXml
include XML::Mapping

text_node :name, "name"
hash_node :addresses, "address", "@name", :class => AddressXml
end


class AddressXml
include XML::Mapping
  
text_node :street_address, "street_address"
text_node :city, "city"
text_node :state, "state"
numerical_node :zip_code, "zip_code"
text_node :name, "@name" 
end
这样也可使 PersonXml#hash_node 不再抱怨。

至止,您已经轻松地将 XML::Mapping 模块包含到 wrapper 类中,并使用混合方法将 XML 节点映射到我们的对象中。text_node 将一个 XPath 路径(如“name”)映射到一个对象属性(如 :name)。hash_node 允许您将对象的散列(这里是 Address 对象)映射到一个属性(如 :addresses)。对于 hash_node,第三个参数充当散列的关键字。在上述例子中,“@name”是 address 元素的 name 属性的 XPath 路径。我们马上就会看到散列的关键字了。

XML::Mapping 将许多节点类型(包括 text_node、numerical_node、hash_node 和 array_node)混合进来。更好的是,您还可创建自己的节点类型。这超出了本文的范围,如想了解,可参阅该模块的自述文件,这是一个很强的特性,是 ROXML 所无法企及的。

现在,让我们暂时忽略一下该实现的 ActiveRecord 片段,看一下您的 XML wrapper 类带来了些什么。通过以下调用来加载我们的 XML 文档:

     
pxml = PersonXml.load_from_file('path_to_xml_file')
当然,如有必要,您需要确定在该调用之前已通过调用 require 定义了 PersonXml。

一旦加载了 PersonXml,您就能够访问绑定到对象上的所有数据了。为此,最简单的方法是使用下例中的一些调用来进行访问:

pxml.name
=> "Ruby Jones"

pxml.addresses['home'].city
=> "Anytown"

pxml.addresses
=> {"home"=>#<Address:0x57f738 @street_address="1200 Main Street", 
@state="SD", @zip_code="12345", @city="Anytown">, 
"work"=>#<Address:0x50b860 @street_address="9898 Center Street", 
@state="SD", @zip_code="11223", @city="Anotherville">}
至此,XML 数据已映射到 wrapper 对象上,可以继续将该数据传递给 ActiveRecord 对象了。(能跳过这个额外的步骤是件很好的事情,知道为什么吗?尽管这只是您所要进行的工作中的最少量工作。)
a_person = Person.new( :name => pxml.name,
:age => pxml.age )

a_person.addresses << Address.new( :street_address => pxml.addresses['home'].street_address,
:city => pxml.addresses['home'].city,
:state => pxml.addresses['home'].state,
:zip_code => pxml.addresses['home'].zip_code,
:name => pxml.addresses['home'].name )

a_person.save!
通过上述代码,您打开并解析了 XML 文件,将数据映射到了 XML wrapper 对象以使数据易于处理,将 XML wrapper 对象属性映射到了 ActiveRecord 模型,并最终将模型保存到了数据库中!

想用更少的代码完成同样的事情吗?

     
[:name, :age].each{|attr| a_person.send(("#{attr}="), pxml.send(attr))}
下面的任务您可以自己来完成:用本例提供的方法添加一个地址。

实现举例

前面我们讲了怎样一起使用 XML::Mapping gem 和 ActiveRecord,现在让我们看一看怎样在一个真实的应用程序实现这点。下面我们将使用一个 Discographr 应用程序(参见这里);如果您对此不熟悉,请返回复习一下该应用程序。现在我们开始集成一个 XML 文件,该文件是由 Last.fm 的优秀 Web 服务提供的。Last.fm 是一个音乐爱好者的网络交流应用程序。该应用程序允许用户自动提交乐曲名并保存其个人音乐收听历史。它还允许用户对包括从歌唱家到乐曲的任何事物打标签。

Discographr 将用一个 XML 文档(该文档由 Last.fm 的 RESTful Web 服务生成)来将音乐专辑及其详细资料添加到它的数据库中。当用户以 Web 的形式输入专辑名和歌唱家时,该功能可提取出该专辑的包括歌曲在内的所有数据。(这里我们要提一下,除非得到Last.fm 的许可,Last.fm 的 Web 服务只用于非商业用途。)

现在以一个 XML 文档来开始我们工作。通过对 http://ws.audioscrobbler.com/1.0/album/Pinback/Offcell/info.xml 的一个 RESTful 调用生成一个 XML 文档,该请求生成的 XML 如下:
<?xml version="1.0" encoding="UTF-8" ?>
<album artist="Pinback" title="Offcell">
<reach>6325</reach>
<url>http://www.last.fm/music/Pinback/Offcell</url>
<releasedate>    10 Jun 2003, 00:00</releasedate>
<coverart>
<small>http://images.amazon.com/images/P/B00009EIS9.01._SCMZZZZZZZ_.jpg</small>
<medium>http://images.amazon.com/images/P/B00009EIS9.01._SCMZZZZZZZ_.jpg</medium>
<large>http://images.amazon.com/images/P/B00009EIS9.01._SCMZZZZZZZ_.jpg</large>
</coverart>
<mbid>28cc2841-46e4-40ab-b371-989a749a8368</mbid>
<tracks>
<track title="Microtonic Wave">
<reach>5226</reach>
<url>http://www.last.fm/music/Pinback/_/Microtonic+Wave</url>
</track>
<track title="Victorious D">
<reach>3521</reach>
<url>http://www.last.fm/music/Pinback/_/Victorious+D</url>
</track>
<track title="Offcell">
<reach>4894</reach>
<url>http://www.last.fm/music/Pinback/_/Offcell</url>
</track>
<track title="B">
<reach>5375</reach>
<url>http://www.last.fm/music/Pinback/_/B</url>
</track>
<track title="Grey Machine">
<reach>3495</reach>
<url>http://www.last.fm/music/Pinback/_/Grey+Machine</url>
</track>
</tracks>
</album>
在编写任何代码之前,您需要做一些初始的设置工作。由于所有 XML wrapper 类依赖于 XML::Mapping 模块,请在 RAILS_ROOT/config/environment.rb 文件未尾添加一条 require_gem 语句:
                     
require_gem 'xml-mapping'
在对该 XML 数据进行更多处理之前,您需要创建 XML wrapper 对象以便提取数据。为此,先在 RAILS_ROOT/lib 下创建一个模块。只要文件/目录名是类/模块名的小写形式,Rails 就会自动加载类定义。(如果由于某些原因您决定不按此规定来进行,始终可以在 environment.rb 中显式地要求文件),所以,在 lib 下,创建一个名为 last_fm_album_pull 的文件夹。然后,创建一个名为 album_xml.rb 的文件,并将下列代码写入其中:
    
module LastFmAlbumPull
  
class AlbumXml
include XML::Mapping
  
text_node :release_name, "@title"
text_node :releasedate, "releasedate"
text_node :artist, "@artist"
array_node :tracks, "tracks", "track", :class => LastFmAlbumPull::SongXml
  
end
在同一目录中创建另一个名为 song_xml.rb 的文件,并将如下代码写入其中:
module LastFmAlbumPull

class SongXml
include XML::Mapping

text_node :title, "@title"

end

end
您使用一个模块为 XML 包装器指定命名空间,以便在结束对其他数据推拉的添加后,可以在之前用一个不同的模块名来使用这些相同的名称。同时,这提高了代码的可移植性。通过使所有类整齐地保持在同一个模块中,您可以轻松地将此模块从 lib 目录中取出,并用它在另一个应用程序中映射相同的 XML 数据。您可能还想将查询 Web 服务的代码包装到该模块中,但现在还是手工来做吧。

现在,您可以将 XML 包装器属性映射到相应的 ActiveRecord 模型。启动控制台,测验我们的数据提取功能:

     
$ script/console
Loading development environment.        
首先,加载带有 XML 文件数据(来自 Last.fm)的 wrapper 对象:
>> lfm_album =  LastFmAlbumPull::AlbumXml.load_from_file(File.join(RAILS_ROOT, "album.xml"))
=> #<LastFmAlbumPull::AlbumXml:0x31d35a8 @artist="Pinback", @releasedate="10 Jun 2003, 00:00", 
@tracks=[#<LastFmAlbumPull::SongXml:0x31d0614 @title="Microtonic Wave">, 
#<LastFmAlbumPull::SongXml:0x31cf9bc @title="Victorious D">, 
#<LastFmAlbumPull::SongXml:0x31cebe8 @title="Offcell">, 
#<LastFmAlbumPull::SongXml:0x31ce300 @title="B">, 
#<LastFmAlbumPull::SongXml:0x31cde28 @title="Grey Machine">], 
@release_name="Offcell">
                            
成功加载之后,通过将 wrapper 对象属性映射到 ActiveRecord 模型属性,来创建一个 ActiveRecord 专辑。请留意对 release_date 字段的处理。您可能已选择在 wrapper 对象中这样做了,但让接收数据的类来决定格式化数据的方式可能更为合适一些。您也许想将 LastFmAlbumPull 模块移到另一个需要完整日期而不是只需要年份的应用程序中。
     
>> album = Album.create( :release_name => lfm_album.release_name, 
:year => lfm_album.releasedate.strip.to_date.year,
:artist => Artist.find_or_create_by_name(lfm_album.artist) )
=> #<Album:0x3214af8 @artist=#<Artist:0x3214990 @attributes={"created_on"=>Tue Feb 27 13:47:24 PST 2007, "name"=>"Pinback", "updated_on"=>Tue Feb 27 13:47:24 PST 2007, "id"=>10202}>, @new_record=false, @new_record_before_save=true, @errors=#<ActiveRecord::Errors:0x3210c28 @base=#<Album:0x3214af8 ...>, @errors={}>, @attributes={"created_on"=>Tue Feb 27 14:30:18 PST 2007, "artist_id"=>10202, "updated_on"=>Tue Feb 27 14:30:18 PST 2007, "id"=>10064, "year"=>2003, "release_name"=>"Offcell"}>
Next, iterate through the tracks (which are objects as created by the wrapper classes).在此,您将为专辑添加若干歌曲,再次将 wrapper 类属性映射到 ActiveRecord 的模型属性。您会将 Song#length 设置为零,因为 Web 服务不提供该信息。
>> lfm_album.tracks.each_with_index do |track, index|                         
?> album.songs << Song.new( :title => track.title,
:track_number => index + 1, :length => 0 )
>> end
=> [#<LastFmAlbumPull::SongXml:0x31cfc14 @title="Microtonic Wave">, #<LastFmAlbumPull::SongXml:0x31cef80 @title="Victorious D">, #<LastFmAlbumPull::SongXml:0x31ce648 @title="Offcell">, #<LastFmAlbumPull::SongXml:0x31cdf7c @title="B">, #<LastFmAlbumPull::SongXml:0x31cdc20 @title="Grey Machine">]
最后,将该专辑对象保存到数据库中!
     
>> album.save
=> true
                            
至此,您只用了不到 20 行的代码就获得 XML 并将其放入 Oracle 数据库中 — 而且这些代码还包括了 XML wrapper 类的额外开销!另外,您还完全拥有了 ActiveRecord 的强大功能(模型定义只有 12 行代码!)当然,这里少了许多东西 — 在实际应用程序中,还应该为模型添加验证代码。但不管怎样,即使添加了验证代码,这些难以置信的功能所需的代码量也是最小的。

利用该技术,您可以毫不费劲地生产出丰富的功能。使用该技术,您可在 Rails 或非 Rails 环境中创建 RESTful Web 服务接口,解析 RSS 输入,处理外部订单数据,以及完成其他任何您所能想到的事情。


Matt Kern 多年来一直致力于寻求和开发通过技术(如 Rail)让生活简单化的方法,主要是试图寻求有更多的时间来与家人游览俄勒冈州中部山峰的方法。他是 Artisan Technologies Inc. 的创始人,并且是 Atlanta PHP 的共同创始人。