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

事务的执行会延迟到块退出,并且是原子执行的(全有或全无):要么执行所有事务调用,要么都不执行。这保持了存储的完整性。

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

事务块

  • 可能不包含对 transaction 的嵌套调用。

  • 是唯一允许从存储读取或写入存储的方法的上下文。

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

  • 方法 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 存储有价值的数据,那么您应该不时备份 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 来发布更改。文件完整性得以维护。注意:仅当文件系统具有原子文件重命名时才有效(如 POSIX 平台 Linux、MacOS、FreeBSD 和其他平台)。

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

公共类方法

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 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 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 pstore.rb, line 459
def []=(key, value)
  in_transaction_wr
  @table[key] = value
end
abort() 点击切换源代码

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

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

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

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

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

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

如果 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
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 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 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 pstore.rb, line 490
def keys
  in_transaction
  @table.keys
end
别名:roots
path() 点击切换源代码

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

store.path # => "flat.store"
# File 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 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 pstore.rb, line 728
def empty_marshal_checksum
  EMPTY_MARSHAL_CHECKSUM
end
empty_marshal_data() 点击切换源代码
# File pstore.rb, line 725
def empty_marshal_data
  EMPTY_MARSHAL_DATA
end
in_transaction() 点击切换源代码

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

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