класс 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.
-
Methodcommitзапускает обновление хранилища и завершает блок: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 -
Methodabortотбрасывает обновление хранилища и завершает блок: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"
Безопасность транзакций
Для обеспечения безопасности транзакций см.:
-
Необязательный аргумент
thread_safeв методеPStore.new. -
Атрибут
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.Fileцелостность сохраняется. Примечание: действует только в случае, если файловая система поддерживает атомарное переименование файлов (как в POSIX-платформах Linux, MacOS, FreeBSD и других). -
false(по умолчанию): изменения публикуются путем перемотки открытого файла и записи обновленных данных.Fileцелостность сохраняется, если файловая система не генерирует непредвиденные ошибки ввода-вывода; если при записи в хранилище произойдет такая ошибка, файл может быть поврежден.
Публичные методы класса
# 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 Возвращает новый объект PStore.
Аргумент file — путь к файлу, в котором будут храниться объекты; если файл существует, он должен быть создан объектом PStore.
path = 't.store' store = PStore.new(path)
Объект PStore является реентерабельным. Если аргумент thread_safe задан как true, объект также является потокобезопасным (с небольшой потерей производительности):
store = PStore.new(path, true)
Публичные методы экземпляра
# File lib/pstore.rb, line 417 def [](key) in_transaction @table[key] 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 459 def []=(key, value) in_transaction_wr @table[key] = value end
Создает или заменяет значение для заданного key.
example_store do |store|
temp.transaction do
temp[:bat] = 3
end
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
# File lib/pstore.rb, line 388 def in_transaction raise PStore::Error, "not in transaction" unless @lock.locked? end
Вызывает PStore::Error, если вызывающий код не находится в PStore#transaction.
# File lib/pstore.rb, line 395 def in_transaction_wr in_transaction raise PStore::Error, "in read-only transaction" if @rdonly end
Вызывает PStore::Error, если вызывающий код не находится в PStore#transaction или если код находится в режиме только для чтения PStore#transaction.
# 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 Загрузить указанный файл PStore. Если read_only истинно, будет возвращён распакованный Hash. Если read_only ложно, будет возвращён кортеж из 3 элементов: распакованный Hash, контрольная сумма данных и размер данных.
# 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 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 Открыть указанный файл filename (в режиме только для чтения или чтения/записи) и заблокировать его для чтения или записи.
Будет возвращён объект открытого File файла. Если read_only истинен, а файл не существует, то будет возвращено nil.
Все исключения передаются дальше.
# 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
Ruby Core © 1993–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.