类 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。
条目¶ ↑
存储可以包含任意数量的条目。每个条目都有一个键和一个值,就像哈希表一样
-
键:就像在哈希表中一样,键可以是(几乎)任何对象;参见 哈希键。您可以发现使用符号或字符串作为键会很方便。
-
值:值可以是任何可以被 Marshal 序列化(参见 Marshal::dump)的对象,实际上可以是一个集合(例如,数组、哈希表、集合、范围等)。该集合可能反过来包含嵌套对象,包括集合,到任何深度;这些对象也必须是可序列化的。参见 分层值。
事务¶ ↑
事务块¶ ↑
使用对方法 transaction
的调用给出的块包含一个事务,该事务包含对从存储中读取或写入存储的 PStore 方法的调用(即,所有 PStore 方法,除了 transaction
本身、path
和 Pstore.new)
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
提前退出块。
-
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
-
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 对象。
参数 file
是要存储对象的文件的路径;如果文件存在,则它应该是 PStore 写入的文件。
path = 't.store' store = PStore.new(path)
PStore 对象是 可重入的。如果参数 thread_safe
被设置为 true
,则该对象也是线程安全的(以牺牲少量性能为代价)。
store = PStore.new(path, true)
# File lib/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 lib/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 lib/pstore.rb, line 459 def []=(key, value) in_transaction_wr @table[key] = value end
退出当前事务块,丢弃在事务块中指定的任何更改。
如果在事务块之外调用,则会引发异常。
# File lib/pstore.rb, line 535 def abort in_transaction @abort = true throw :pstore_abort_transaction end
退出当前事务块,提交在事务块中指定的任何更改。
如果在事务块之外调用,则会引发异常。
# File lib/pstore.rb, line 524 def commit in_transaction @abort = false throw :pstore_abort_transaction end
如果存在,则删除并返回key
处的 value
example_store do |store| store.transaction do store[:bat] = 3 store.delete(:bat) end end
如果不存在此键,则返回nil
。
如果在事务块之外调用,则会引发异常。
# File lib/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 lib/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 lib/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 lib/pstore.rb, line 490 def keys in_transaction @table.keys end
返回用于创建存储的字符串文件路径
store.path # => "flat.store"
# File lib/pstore.rb, line 515 def path @filename end
为存储打开一个事务块。参见 事务。
当参数 read_only
为 false
时,该块可以从存储中读取数据,也可以写入数据。
当参数 read_only
为 true
时,该块不能包含对 transaction
、[]=
或 delete
的调用。
如果在事务块中调用,则会引发异常。
# File lib/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 lib/pstore.rb, line 728 def empty_marshal_checksum EMPTY_MARSHAL_CHECKSUM end
# File lib/pstore.rb, line 725 def empty_marshal_data EMPTY_MARSHAL_DATA end
如果调用代码不在 PStore#transaction
中,则会引发 PStore::Error
。
# File lib/pstore.rb, line 388 def in_transaction raise PStore::Error, "not in transaction" unless @lock.locked? end
如果调用代码不在 PStore#transaction
中,或者代码在只读 PStore#transaction
中,则会引发 PStore::Error
。
# File lib/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 lib/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 lib/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 为真,并且文件不存在,则将返回 nil。
所有异常都会被传播。
# File lib/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 lib/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 lib/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 lib/pstore.rb, line 706 def save_data_with_fast_strategy(data, file) file.rewind file.write(data) file.truncate(data.bytesize) end