class PStore
PStore 实现了一种基于 Hash 的文件持久化机制。用户代码可以通过名称(键)将 Ruby 对象(值)的层次结构存储到数据存储中。对象层次结构可以只是一个单独的对象。用户代码稍后可以从数据存储中读取值,甚至可以根据需要更新数据。
事务行为确保任何更改都会一起成功或失败。这可以用来确保数据存储不会处于临时状态,即某些值已更新而其他值未更新。
在幕后,Ruby 对象使用 Marshal 存储到数据存储文件中。这带有通常的限制。例如,Proc 对象无法被编组。
这里有三个重要的概念(详情请查看链接)
-
存储:存储是 PStore 的一个实例。
-
条目:存储类似哈希;每个条目都是存储对象的键。
-
事务:每个事务都是对存储的预期更改的集合;事务在调用
PStore#transaction
时给定的块中定义。
关于示例¶ ↑
此页面上的示例需要一个具有已知属性的存储。它们可以通过调用以下内容来获取新的(和填充的)存储
example_store do |store| # Example code using store goes here. end
我们真正需要了解 example_store
的是,它会产生一个具有已知条目填充的新存储;它的实现
require 'pstore' require 'tempfile' # Yield a pristine store for use in examples. def example_store # Create the store in a temporary file. Tempfile.create do |file| store = PStore.new(file) # Populate the store. store.transaction do store[:foo] = 0 store[:bar] = 1 store[:baz] = 2 end yield store end end
存储¶ ↑
存储的内容保存在一个文件中,该文件的路径在创建存储时指定(请参阅 PStore.new
)。对象使用模块 Marshal 进行存储和检索,这意味着某些对象无法添加到存储中;请参阅 Marshal::dump。
条目¶ ↑
一个存储可以有任意数量的条目。每个条目都有一个键和一个值,就像在哈希中一样
-
键:就像在哈希中一样,键可以是(几乎)任何对象;请参阅 Hash。您可能会发现只使用符号或字符串作为键会很方便。
-
值:该值可以是任何可以被 Marshal 编组的对象(请参阅 Marshal::dump),并且实际上可以是集合(例如,数组、哈希、集合、范围等)。该集合又可以包含嵌套的对象,包括任何深度的集合;这些对象也必须是可 Marshal 的。请参阅 分层值。
事务¶ ↑
事务块¶ ↑
在调用 transaction
方法时给定的块包含一个事务,该事务包含对 PStore 方法的调用,这些方法从存储中读取或写入存储(即,除了 transaction
本身、path
和 Pstore.new 之外的所有 PStore 方法)
example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] store[:bat] = 3 store.keys # => [:foo, :bar, :baz, :bat] end end
事务的执行会延迟到块退出,并且是原子执行的(全有或全无):要么执行所有事务调用,要么都不执行。这保持了存储的完整性。
块中的其他代码(甚至包括对 path
和 PStore.new
的调用)会立即执行,而不是延迟执行。
事务块
-
可能不包含对
transaction
的嵌套调用。 -
是唯一允许从存储读取或写入存储的方法的上下文。
如上所述,当块退出时,事务中的更改会自动进行。可以通过调用方法 commit
或 abort
来提前退出块。
-
方法
commit
会触发对存储的更新并退出块example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] store[:bat] = 3 store.commit fail 'Cannot get here' end store.transaction do # Update was completed. store.keys # => [:foo, :bar, :baz, :bat] end end
-
方法
abort
会丢弃对存储的更新并退出块example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] store[:bat] = 3 store.abort fail 'Cannot get here' end store.transaction do # Update was not completed. store.keys # => [:foo, :bar, :baz] end end
只读事务¶ ↑
默认情况下,事务允许从存储读取和写入存储
store.transaction do # Read-write transaction. # Any code except a call to #transaction is allowed here. end
如果参数 read_only
作为 true
传递,则只允许读取
store.transaction(true) do # Read-only transaction: # Calls to #transaction, #[]=, and #delete are not allowed here. end
分层值¶ ↑
条目的值可以是一个简单的对象(如上所述)。它也可以是嵌套到任何深度的对象层次结构
deep_store = PStore.new('deep.store') deep_store.transaction do array_of_hashes = [{}, {}, {}] deep_store[:array_of_hashes] = array_of_hashes deep_store[:array_of_hashes] # => [{}, {}, {}] hash_of_arrays = {foo: [], bar: [], baz: []} deep_store[:hash_of_arrays] = hash_of_arrays deep_store[:hash_of_arrays] # => {:foo=>[], :bar=>[], :baz=>[]} deep_store[:hash_of_arrays][:foo].push(:bat) deep_store[:hash_of_arrays] # => {:foo=>[:bat], :bar=>[], :baz=>[]} end
请记住,您可以在返回的对象层次结构中使用 dig 方法。
使用存储¶ ↑
创建存储¶ ↑
使用方法 PStore.new
创建存储。新存储会创建或打开其包含文件
store = PStore.new('t.store')
修改存储¶ ↑
使用方法 []=
更新或创建条目
example_store do |store| store.transaction do store[:foo] = 1 # Update. store[:bam] = 1 # Create. end end
使用方法 delete
删除条目
example_store do |store| store.transaction do store.delete(:foo) store[:foo] # => nil end end
检索值¶ ↑
使用方法 fetch
(允许默认值)或 []
(默认为 nil
)检索条目
example_store do |store| store.transaction do store[:foo] # => 0 store[:nope] # => nil store.fetch(:baz) # => 2 store.fetch(:nope, nil) # => nil store.fetch(:nope) # Raises exception. end end
查询存储¶ ↑
使用方法 key?
确定给定的键是否存在
example_store do |store| store.transaction do store.key?(:foo) # => true end end
使用方法 keys
检索键
example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] end end
使用方法 path
检索存储的底层文件的路径;此方法可以在事务块之外调用
store = PStore.new('t.store') store.path # => "t.store"
事务安全性¶ ↑
有关事务安全性,请参阅
-
方法
PStore.new
的可选参数thread_safe
。 -
属性
ultra_safe
。
不用说,如果您使用 PStore 存储有价值的数据,那么您应该不时备份 PStore 文件。
示例存储¶ ↑
require "pstore" # A mock wiki object. class WikiPage attr_reader :page_name def initialize(page_name, author, contents) @page_name = page_name @revisions = Array.new add_revision(author, contents) end def add_revision(author, contents) @revisions << {created: Time.now, author: author, contents: contents} end def wiki_page_references [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/) end end # Create a new wiki page. home_page = WikiPage.new("HomePage", "James Edward Gray II", "A page about the JoysOfDocumentation..." ) wiki = PStore.new("wiki_pages.pstore") # Update page data and the index together, or not at all. wiki.transaction do # Store page. wiki[home_page.page_name] = home_page # Create page index. wiki[:wiki_index] ||= Array.new # Update wiki index. wiki[:wiki_index].push(*home_page.wiki_page_references) end # Read wiki data, setting argument read_only to true. wiki.transaction(true) do wiki.keys.each do |key| puts key puts wiki[key] end end
常量
- CHECKSUM_ALGO
用于缓解 Ruby 垃圾收集器的常量。
- EMPTY_MARSHAL_CHECKSUM
- EMPTY_MARSHAL_DATA
- EMPTY_STRING
- RDWR_ACCESS
- RD_ACCESS
- VERSION
- WR_ACCESS
属性
即使发生不太可能的错误(例如内存错误或文件系统错误),PStore 是否应尽最大努力防止文件损坏
-
true
:通过创建临时文件、将更新的数据写入其中,然后将该文件重命名为给定的path
来发布更改。文件完整性得以维护。注意:仅当文件系统具有原子文件重命名时才有效(如 POSIX 平台 Linux、MacOS、FreeBSD 和其他平台)。 -
false
(默认值):通过回溯打开的文件并写入更新的数据来发布更改。如果文件系统没有引发意外的 I/O 错误,则文件完整性得以维护;如果在写入存储期间发生此类错误,则该文件可能会损坏。
公共类方法
返回一个新的 PStore 对象。
参数 file
是要存储对象的文件的路径;如果文件存在,则它应该是 PStore 写入的文件。
path = 't.store' store = PStore.new(path)
PStore 对象是 可重入的。如果参数 thread_safe
给定为 true
,则该对象也是线程安全的(以牺牲少量性能为代价)
store = PStore.new(path, true)
# File pstore.rb, line 372 def initialize(file, thread_safe = false) dir = File::dirname(file) unless File::directory? dir raise PStore::Error, format("directory %s does not exist", dir) end if File::exist? file and not File::readable? file raise PStore::Error, format("file %s not readable", file) end @filename = file @abort = false @ultra_safe = false @thread_safe = thread_safe @lock = Thread::Mutex.new end
公共实例方法
如果该键存在,则返回给定 key
的值。否则返回 nil
;如果不是 nil
,则返回的值是一个对象或对象的层次结构
example_store do |store| store.transaction do store[:foo] # => 0 store[:nope] # => nil end end
如果不存在这样的键,则返回 nil
。
另请参阅 分层值。
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 417 def [](key) in_transaction @table[key] end
创建或替换给定 key
的值
example_store do |store| temp.transaction do temp[:bat] = 3 end end
另请参阅 分层值。
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 459 def []=(key, value) in_transaction_wr @table[key] = value end
退出当前事务块,丢弃在 事务块 中指定的任何更改。
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 535 def abort in_transaction @abort = true throw :pstore_abort_transaction end
退出当前事务块,提交在 事务块 中指定的任何更改。
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 524 def commit in_transaction @abort = false throw :pstore_abort_transaction end
如果 key
存在,则删除并返回其值
example_store do |store| store.transaction do store[:bat] = 3 store.delete(:bat) end end
如果不存在这样的键,则返回 nil
。
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 476 def delete(key) in_transaction_wr @table.delete key end
与 []
类似,只是它接受存储的默认值。如果 key
不存在
-
如果
default
为PStore::Error
,则引发异常。 -
否则返回
default
的值example_store do |store| store.transaction do store.fetch(:nope, nil) # => nil store.fetch(:nope) # Raises an exception. end end
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 436 def fetch(key, default=PStore::Error) in_transaction unless @table.key? key if default == PStore::Error raise PStore::Error, format("undefined key '%s'", key) else return default end end @table[key] end
如果 key
存在,则返回 true
,否则返回 false
example_store do |store| store.transaction do store.key?(:foo) # => true end end
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 505 def key?(key) in_transaction @table.key? key end
返回现有键的数组
example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] end end
如果在事务块之外调用,则会引发异常。
# File pstore.rb, line 490 def keys in_transaction @table.keys end
返回用于创建存储的字符串文件路径
store.path # => "flat.store"
# File pstore.rb, line 515 def path @filename end
为存储打开一个事务块。请参阅 事务。
如果参数 read_only
为 false
,则该块可以读取和写入存储。
如果参数 read_only
为 true
,则该块不能包含对 transaction
、[]=
或 delete
的调用。
如果在事务块内调用,则会引发异常。
# File pstore.rb, line 551 def transaction(read_only = false) # :yields: pstore value = nil if !@thread_safe raise PStore::Error, "nested transaction" unless @lock.try_lock else begin @lock.lock rescue ThreadError raise PStore::Error, "nested transaction" end end begin @rdonly = read_only @abort = false file = open_and_lock_file(@filename, read_only) if file begin @table, checksum, original_data_size = load_data(file, read_only) catch(:pstore_abort_transaction) do value = yield(self) end if !@abort && !read_only save_data(checksum, original_data_size, file) end ensure file.close end else # This can only occur if read_only == true. @table = {} catch(:pstore_abort_transaction) do value = yield(self) end end ensure @lock.unlock end value end
私有实例方法
# File pstore.rb, line 728 def empty_marshal_checksum EMPTY_MARSHAL_CHECKSUM end
# File pstore.rb, line 725 def empty_marshal_data EMPTY_MARSHAL_DATA end
如果调用代码不在 PStore#transaction
中,则引发 PStore::Error
。
# File pstore.rb, line 388 def in_transaction raise PStore::Error, "not in transaction" unless @lock.locked? end
如果调用代码不在 PStore#transaction
中,或者代码在只读 PStore#transaction
中,则引发 PStore::Error
。
# File pstore.rb, line 395 def in_transaction_wr in_transaction raise PStore::Error, "in read-only transaction" if @rdonly end
加载给定的 PStore
文件。如果 read_only
为 true,则返回反序列化的 Hash。如果 read_only
为 false,则返回一个 3 元组:反序列化的 Hash、数据的校验和以及数据的大小。
# File pstore.rb, line 639 def load_data(file, read_only) if read_only begin table = load(file) raise Error, "PStore file seems to be corrupted." unless table.is_a?(Hash) rescue EOFError # This seems to be a newly-created file. table = {} end table else data = file.read if data.empty? # This seems to be a newly-created file. table = {} checksum = empty_marshal_checksum size = empty_marshal_data.bytesize else table = load(data) checksum = CHECKSUM_ALGO.digest(data) size = data.bytesize raise Error, "PStore file seems to be corrupted." unless table.is_a?(Hash) end data.replace(EMPTY_STRING) [table, checksum, size] end end
# File pstore.rb, line 667 def on_windows? is_windows = RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince/ self.class.__send__(:define_method, :on_windows?) do is_windows end is_windows end
以只读模式或读写模式打开指定的文件名,并锁定以进行读取或写入。
将返回打开的 File 对象。如果 read_only 为 true,且该文件不存在,则将返回 nil。
所有异常都会传播。
# File pstore.rb, line 614 def open_and_lock_file(filename, read_only) if read_only begin file = File.new(filename, **RD_ACCESS) begin file.flock(File::LOCK_SH) return file rescue file.close raise end rescue Errno::ENOENT return nil end else file = File.new(filename, **RDWR_ACCESS) file.flock(File::LOCK_EX) return file end end
# File pstore.rb, line 675 def save_data(original_checksum, original_file_size, file) new_data = dump(@table) if new_data.bytesize != original_file_size || CHECKSUM_ALGO.digest(new_data) != original_checksum if @ultra_safe && !on_windows? # Windows doesn't support atomic file renames. save_data_with_atomic_file_rename_strategy(new_data, file) else save_data_with_fast_strategy(new_data, file) end end new_data.replace(EMPTY_STRING) end
# File pstore.rb, line 690 def save_data_with_atomic_file_rename_strategy(data, file) temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}" temp_file = File.new(temp_filename, **WR_ACCESS) begin temp_file.flock(File::LOCK_EX) temp_file.write(data) temp_file.flush File.rename(temp_filename, @filename) rescue File.unlink(temp_file) rescue nil raise ensure temp_file.close end end
# File pstore.rb, line 706 def save_data_with_fast_strategy(data, file) file.rewind file.write(data) file.truncate(data.bytesize) end