10.2 执行高级数据访问
经常要以更透明的方式存储和检索数据,Marshal模块提供了简单的对象持久化功能,PStore库建立在这项功能的基础上。最后,dbm可以像散列一样使用,但永久地存储在磁盘中。它其实并不属于本节讨论的内容,但它太简单,不适合将其放在有关数据库的一节中讨论。
10.2.1 简单序列化
很多情况下,需要创建一个对象,然后将它保存起来供以后使用。Ruby为这种对象持久化或称为序列化(marshaling)提供了基本支持。Marshal模块让程序能够以这种方式序列化和反序列化Ruby对象。
# array of elements [composer, work, minutes]
works = [["Leonard Bernstein","Overture to Candide",11],
["Aaron Copland","Symphony No. 3",45],
["Jean Sibelius","Finlandia",20]]
# We want to keep this for later...
File.open("store","w") do |file|
Marshal.dump(works,file)
end
# Much later...
File.open("store") do |file|
works = Marshal.load(file)
end
这种方式的缺点是,并非所有对象都可被序列化(dump)。如果对象包含低级类的对象,就不能序列化,这些低级类包括IO、Proc和Binding。单例对象、匿名类及模块也不能序列化。
还可以其他两种形式给Marshal.dump参数传递。如果调用时只指定了一个参数,它将以字符串方式返回数据,其中前两个字节为主版本号和次版本号。
s = Marshal.dump(works)
p s[0] # 4
p s[1] # 8
正常情况下,如果试图载入这种数据,仅当主版本号相等且当前的次版本号小于或等于数据中的次版本号时才载入。然而,如果设置了Ruby解释器的verbose标记(使用-verbose或-v),则版本号必须完全匹配。这些版本号独立于Ruby版本号。
第三个参数limit仅当要序列化的对象包含嵌套对象时才有意义。如果给Marshal.dump指定了这个参数(整数),则遍历要序列化的对象时,将使用它作为最大遍历深度。如果嵌套的深度小于指定的界限,对象的序列化将不会发生错误;否则将引发ArgumentError。看看下面这个例子将更清楚这一点:
File.open("store","w") do |file|
arr = [ ]
Marshal.dump(arr,file,0) # in `dump': exceed depth limit
# (ArgumentError)
Marshal.dump(arr,file,1)
arr = [1, 2, 3]
Marshal.dump(arr,file,1) # in `dump': exceed depth limit
# (ArgumentError)
Marshal.dump(arr,file,2)
arr = [1, [2], 3]
Marshal.dump(arr,file,2) # in `dump': exceed depth limit
# (ArgumentError)
Marshal.dump(arr,file,3)
end
File.open("store") do |file|
p Marshal.load(file) # [ ]
p Marshal.load(file) # [1, 2, 3]
p Marshal.load(file) # arr = [1, [2], 3]
end
第三个参数的默认值为−1,负深度表示不检查深度。
10.2.2 更复杂的序列化
有时需要对序列化进行定制,为此可创建方法_load和_dump。序列化完成后,将调用这些方法,这样程序员可以执行自己的对象和字符串之间的转换。
在下面的例子中,有个人出生后就一直从存款中赚取5%的利息。这里没有保存年龄和当前余额,因为它是时间的函数。
class Person
attr_reader :name
attr_reader :age
attr_reader :balance
def initialize(name,birthdate,beginning)
@name = name
@birthdate = birthdate
@beginning = beginning
@age = (Time.now - @birthdate)/(365*86400)
@balance = @beginning*(1.05**@age)
end
def marshal_dump
Struct.new("Human",:name,:birthdate,:beginning)
str = Struct::Human.new(@name,@birthdate,@beginning)
str
end
def marshal_load(str)
self.instance_eval do
initialize(str.name, str.birthdate, str.beginning)
end
end
# Other methods...
end
p1 = Person.new("Rudy",Time.now - (14 * 365 * 86400), 100)
p [p1.name, p1.age, p1.balance] # ["Rudy", 14.0, 197.99315994394]
str = Marshal.dump(p1)
p2 = Marshal.load(str)
p [p2.name, p2.age, p2.balance] # ["Rudy", 14.0, 197.99315994394]
在保存这种类型的对象时,不会保存年龄和当前余额,“重建”对象时将计算这些值。注意,方法marshal_load假设已经有一个对象,这时要显式地调用initialize(和new调用它一样)——其他情况下很少需要这么做。
10.2.3 使用Marshal执行有限的“深拷贝”
Ruby没有“深拷贝”的操作,方法dup和clone的行为可能与你期望的不同。对象可能包含嵌套的对象引用,这使拷贝操作变成Pick-Up-Sticks游戏。
下面提供了一种执行有限深拷贝的方式,由于是基于Marshal的,有其固有局限性,因此其拷贝深度受到限制。
def deep_copy(obj)
Marshal.load(Marshal.dump(obj))
end
a = deep_copy(b)
10.2.4 使用Pstore执行更好的对象持久化
使用PStore库可对Ruby对象进行基于文件的持久化存储。PStore对象可存储大量的Ruby对象层次结构,每个层次结构都有一个用键标识的根(root)。在事务开始时从磁盘读取层次结构,并在事务结束时将其写回磁盘。
require "pstore"
# save
db = PStore.new("employee.dat")
db.transaction do
db["params"] = {"name" => "Fred", "age" => 32,
"salary" => 48000 }
end
# retrieve
require "pstore"
db = PStore.new("employee.dat")
emp = nil
db.transaction { emp = db["params"] }
通常,在事务代码块中使用传入的PStore对象,但也可以像上述代码那样,直接使用接受方。
这种方法是面向事务的,在代码块的开头,从要操作的磁盘文件中读取数据。然后,它们被透明地写回到磁盘。
在事务进行过程中,可使用commit或abort来中断事务,前者保留已做的修改,后者放弃所做的修改。请看下面这个更长的例子:
require "pstore"
# Assume existing file with two objects stored
store = PStore.new("objects")
store.transaction do |s|
a = s["my_array"]
h = s["my_hash"]
# Imaginary code omitted, manipulating
# a, h, etc.
# Assume a variable named "condition" having
# the value 1, 2, or 3...
case condition
when 1
puts "Oops... aborting."
s.abort # Changes will be lost.
when 2
puts "Committing and jumping out."
s.commit # Changes will be saved.
when 3
# Do nothing...
end
puts "We finished the transaction to the end."
# Changes will be saved.
end
在事务中,也可使用方法roots返回一个由根组成的数组(或使用root?测试对象属于哪个根),还有一个delete方法可删除根。
store.transaction do |s|
list = s.roots # ["my_array","my_hash"]
if s.root?("my_tree")
puts "Found my_tree."
else
puts "Didn't find # my_tree."
end
s.delete("my_hash")
list2 = s.roots # ["my_array"]
end
10.2.5 处理CSV数据
如果读者使用过电子表格或数据库,可能处理过CSV(comma-separated values,逗号分隔的数据)格式。幸运的是,Hiroshi Nakamura为Ruby创建了一个模块,并将它放在Ruby Application Archive中。
还有一个由James Edward Gray III创建的FasterCSV库。顾名思义,它的运行更快,但它在接口方面也有些改进(使用其他库的用户可在“兼容模式”下使用它)。编写本书时,有讨论说FasterCSV可能成为标准,并取代旧库(还可能取代它的名称)。
显然这不是真正的数据库系统,但在本章中讨论它比在其他地方讨论它更合适。
CSV模块(csv.rb)解析或生成CSV格式的数据。对CSV数据的准确格式没有达成一致,但FasterCSV库的作者对这种格式的定义如下:
· 记录分隔符:CR+LF;
· 字段分隔符:逗号;
· 如果数据包含CR、LF或逗号,就用双引号将其括起;
· 引用双引号时在它前面再加一个双引号(“ -> “”);
· 带引号的空字段表示空字符串(data,””,data);
· 不带引号的空字段表示NULL(data,,data)。
本节只介绍这个库的部分功能。对初学者来说,这足够了,最新的文档可在网上查找(从ruby-doc.org开始)。
首先创建一个文件。要将逗号分隔的数据写入文件,只需以写入模式打开文件,open方法将一个writer对象传递给代码块,然后使用附加运算符来追加数据数组(写入时将被转换为逗号分隔的格式)。
require 'csv'
CSV.open("data.csv","w") do |wr|
wr << ["name", "age", "salary"]
wr << ["mark", "29", "34500"]
wr << ["joe", "42", "32000"]
wr << ["fred", "22", "22000"]
wr << ["jake", "25", "24000"]
wr << ["don", "32", "52000"]
end
上述代码生成数据文件data.csv:
"name","age","salary"
"mark",29,34500
"joe",42,32000
"fred",22,22000
"jake",25,24000
"don",32,52000
另一个程序可读取该文件,如下所示:
require 'csv'
CSV.open('data.csv', 'r') do |row|
p row
end
# Output:
# ["name", "age", "salary"]
# ["mark", "29", "34500"]
# ["joe", "42", "32000"]
# ["fred", "22", "22000"]
# ["jake", "25", "24000"]
# ["don", "32", "52000"]
上述代码也可编写成不使用代码块的,这样open调用将返回一个reader对象,然后调用reader的shift(将它视为数组)以获取下一行。但面向代码块的方式看起来更直观。
这个库还有一些高级特性和方便的方法,更详细的信息请参阅ruby-doc.org或Ruby Application Archive。
10.2.6 使用YAML进行序列化
据报道,YAML表示YAML Ain’t Markup Language(YAML不是标记语言),它只不过是一种灵活的人类可读的数据存储格式。因此,它类似于XML,但更“优美”。
通过请求(require)yaml库,将给每个对象添加一个to_yaml。作为开始,可序列化一些简单对象和复杂对象,看看YAML如何处理它们。
require 'yaml'
str = "Hello, world"
num = 237
arr = %w[ Jan Feb Mar Apr ]
hsh = {"This" => "is", "just a"=>"hash."}
puts str.to_yaml
puts num.to_yaml
puts arr.to_yaml
puts hsh.to_yaml
# Output:
# --- "Hello, world"
# --- 237
# ---
# - Jan
# - Feb
# - Mar
# - Apr
# ---
# just a: hash.
# This: is
方法YAML.load是to_yaml的逆方法,它接受一个字符串或流作为参数。
假设有如下所示的data.yaml文件:
---
- "Hello, world"
- 237
-
- Jan
- Feb
- Mar
- Apr
-
just a: hash.
This: is
这和刚才看到的四个数据项相同,但它们被放到单个数组中。如果现在就加载这个流,将获得这个数组:
require 'yaml'
file = File.new("data.yaml")
array = YAML.load(file)
file.close
p array
# Output:
# ["Hello, world", 237, ["Jan", "Feb", "Mar", "Apr"],
# {"just a"=>"hash.", "This"=>"is"}]
一般而言,YAML只是对象序列化的一种方式,在更高层次上,它还有很多用途。例如,由于它是人类可读的,因此也是人类可编辑的,自然可作为配置文件的格式。
除这里介绍的外,YAML还有其他特性,更详细的信息请参阅ruby-doc.org或任何书面参考资料。
10.2.7 使用Madeleine进行对象的Prevalence
在有些领域中,对象prevalence很流行。其思想是,内存很便宜且将越来越便宜,而大部分数据库都相当小,因此可将数据库忘在脑后,将所有对象都存储在内存中。
经典实现是使用Java实现的Prevayler,Ruby的版本名为Madeleine。
Madeleine并不适合所有人或所有应用程序。对象prevalence有其一套规则和约束。首先,所有对象都必须能够同时存储到内存中;其次,所有对象都必须是可序列化的(marshalable)。
所有对象都必须是确定的(deterministic),即在输入相同的情况下,它们的行为必须也相同,这意味着不能使用系统时钟或随机数。
对象应尽可能同所有I/O(文件和网络)分离,通常在prevalence系统外调用这些I/O操作。
最后,每个修改prevalence系统状态的命令都必须以命令对象的形式发出(这样,这些对象也可被序列化和存储)。
Madeleine提供了两个访问对象系统的基本方法。execute_query方法提供查询功能或只读访问功能,execute_command方法封装了所有修改对象系统中任何对象状态的操作。
这两个方法都接受一个Command对象作为参数,根据定义,Command对象有一个execute方法。
在应用程序运行过程中,prevalence系统定期地获取对象系统的快照,命令和其他对象一起被序列化,目前无法“回滚”一组事务。
很难创建一个有意义的例子来说明这个库的用法,如果读者熟悉其Java版本,建议通过研究Ruby API来学习它。当前没有有关这方面的优秀教程,也许读者可编写一本。
10.2.8 使用DBM库
DBM是一种简单的基于字符串的散列文件存储机制,它是独立于平台的。它存储键和相关联的数据,键和数据都必须是字符串。Ruby的dbm接口被内置在标准安装中。
要使用这个类,可创建一个和文件名相关联的DBM对象,然后像使用基于字符串的散列那样使用它。处理完毕后,应关闭该文件。
require 'dbm'
d = DBM.new("data")
d["123"] = "toodle-oo!"
puts d["123"] # "toodle-oo!"
d.close
puts d["123"] # RuntimeError: closed DBM file
e = DBM.open("data")
e["123"] # "toodle-oo!"
w=e.to_hash # {"123"=>"toodle-oo!"}
e.close
e["123"] # RuntimeError: closed DBM file
w["123"] # "toodle-oo!
DBM被实现为单个类,这个类混合插入了Enumerable。它的两个类方法new和open(互为别名)是单例方法,这就意味着在给定时刻,每个打开的数据文件只有一个对应的DBM对象。
q=DBM.new("data.dbm") #
f=DBM.open("data.dbm") # Errno::EWOULDBLOCK:
# Try again - "data.dbm"
DBM类有34个实例方法,其中很多是散列方法的别名或与其相似。基本上,如果习惯了以某种方式对真正的散列进行操作,便很可能能够对dbm对象执行相同的操作。
方法to_hash复制内存中的散列文件对象,方法close永久地关闭到散列文件的链接。其他的大部分方法与散列方法类似,但没有方法rehash、sort、default和default=。to_s方法返回对象id的字符串表示。







