类 PStore

PStore 实现了一种基于 Hash 的文件持久化机制。用户代码可以通过名称(键)将 Ruby 对象(值)的层次结构存储到数据存储中。对象层次结构可能只是一个单一对象。用户代码以后可以从数据存储中读取值,甚至根据需要更新数据。

事务行为确保所有更改一起成功或一起失败。这可以用来确保数据存储不会处于过渡状态,即某些值已更新而另一些值未更新。

在幕后,Ruby 对象使用 Marshal 存储到数据存储文件中。这带来了通常的限制。例如,Proc 对象无法被序列化。

这里有三个重要的概念(链接中提供详细信息)

关于示例

本页面的示例需要一个具有已知属性的存储。可以通过以下方式获取一个新的(并填充的)存储

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

条目

存储可以包含任意数量的条目。每个条目都有一个键和一个值,就像哈希表一样

事务

事务块

使用对方法 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

事务执行被延迟到块退出,并且以原子方式(全有或全无)执行:要么所有事务调用都执行,要么都不执行。这维护了存储的完整性。

块中的其他代码(甚至包括对 pathPStore.new 的调用)会立即执行,不会延迟。

事务块

如上所述,事务中的更改将在块退出时自动进行。可以通过调用方法 commitabort 提前退出块。

只读事务

默认情况下,事务允许从存储中读取和写入

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 存储重要数据,那么您应该定期备份 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

属性

ultra_safe[RW]

PStore 是否应尽最大努力防止文件损坏,即使在出现不太可能发生的错误(例如内存错误或文件系统错误)时也是如此。

  • true:通过创建临时文件、将更新后的数据写入临时文件,然后将临时文件重命名为给定的 path 来发布更改。 File 的完整性得到维护。注意:只有在文件系统具有原子文件重命名功能(如 POSIX 平台 Linux、MacOS、FreeBSD 等)时才有效。

  • false(默认值):通过倒带打开的文件并写入更新后的数据来发布更改。如果文件系统没有引发意外的 I/O 错误,则 File 的完整性将得到维护;如果在写入存储库期间发生此类错误,则文件可能会损坏。

公共类方法

new(file, thread_safe = false) 点击以切换源代码

返回一个新的 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) 点击切换源代码

如果键存在,则返回给定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, value) 点击切换源代码

创建或替换给定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
abort() 点击切换源代码

退出当前事务块,丢弃在事务块中指定的任何更改。

如果在事务块之外调用,则会引发异常。

# File lib/pstore.rb, line 535
def abort
  in_transaction
  @abort = true
  throw :pstore_abort_transaction
end
commit() 点击切换源代码

退出当前事务块,提交在事务块中指定的任何更改。

如果在事务块之外调用,则会引发异常。

# File lib/pstore.rb, line 524
def commit
  in_transaction
  @abort = false
  throw :pstore_abort_transaction
end
delete(key) 点击切换源代码

如果存在,则删除并返回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
fetch(key, default=PStore::Error) 点击切换源代码

[]类似,不同之处在于它接受存储的默认值。如果key不存在

  • 如果defaultPStore::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?(key) 点击切换源代码

如果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
也称为:root?
keys() 点击切换源代码

返回现有键的数组

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
也称为:roots
path() 点击切换源代码

返回用于创建存储的字符串文件路径

store.path # => "flat.store"
# File lib/pstore.rb, line 515
def path
  @filename
end
root?(key)
别名:key?
roots()
别名:keys
transaction(read_only = false) { |pstore| ... } 点击切换源代码

为存储打开一个事务块。参见 事务

当参数 read_onlyfalse 时,该块可以从存储中读取数据,也可以写入数据。

当参数 read_onlytrue 时,该块不能包含对 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

私有实例方法

empty_marshal_checksum() 点击切换源代码
# File lib/pstore.rb, line 728
def empty_marshal_checksum
  EMPTY_MARSHAL_CHECKSUM
end
empty_marshal_data() 点击切换源代码
# File lib/pstore.rb, line 725
def empty_marshal_data
  EMPTY_MARSHAL_DATA
end
in_transaction() 点击切换源代码

如果调用代码不在 PStore#transaction 中,则会引发 PStore::Error

# File lib/pstore.rb, line 388
def in_transaction
  raise PStore::Error, "not in transaction" unless @lock.locked?
end
in_transaction_wr() 点击切换源代码

如果调用代码不在 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
load_data(file, read_only) 点击切换源代码

加载给定的 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
on_windows?() 点击切换源代码
# 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
open_and_lock_file(filename, read_only) 点击切换源代码

以只读模式或读写模式打开指定的文件名,并锁定以进行读写操作。

将返回打开的 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
save_data(original_checksum, original_file_size, file) 点击切换源代码
# 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
save_data_with_atomic_file_rename_strategy(data, file) 点击切换源代码
# 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
save_data_with_fast_strategy(data, file) 点击切换源代码
# File lib/pstore.rb, line 706
def save_data_with_fast_strategy(data, file)
  file.rewind
  file.write(data)
  file.truncate(data.bytesize)
end