類別 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 方法,這些方法會從儲存庫中讀取或寫入儲存庫(也就是說,除了 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)會立即執行,不會延遲。

交易區塊

如上所述,交易中的變更會在區塊結束時自動進行。可以呼叫方法 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 處的值(如果存在)

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 為 true,且檔案不存在,則會傳回 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