класс 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); это также может быть коллекция (например, массив, хеш, множество, диапазон и т. д.). Такая коллекция, в свою очередь, может содержать вложенные объекты, в том числе коллекции, на любую глубину; эти объекты тоже должны поддерживать Marshal. См. Иерархические значения.
Транзакции
Блок транзакции
Блок, переданный при вызове метода 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.
-
Метод
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"
Безопасность транзакций
Сведения о безопасности транзакций см. в следующих разделах:
-
Необязательный аргумент
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 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
Завершает текущий блок транзакции, применяя все изменения, указанные в блоке транзакции.
Вызывает исключение при вызове вне блока транзакции.
# File lib/pstore.rb, line 476 def delete(key) in_transaction_wr @table.delete key end
Удаляет и возвращает значение по адресу key, если оно существует:
example_store do |store|
store.transaction do
store[:bat] = 3
store.delete(:bat)
end
end
Возвращает nil, если такого ключа нет.
Вызывает исключение при вызове вне блока транзакции.
# 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 не существует:
-
Вызывает исключение, если
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 505 def key?(key) in_transaction @table.key? key end
Возвращает true, если key существует, и false в противном случае:
example_store do |store|
store.transaction do
store.key?(:foo) # => true
end
end
Вызывает исключение при вызове вне блока транзакции.
# File lib/pstore.rb, line 490 def keys in_transaction @table.keys end
Возвращает массив существующих ключей:
example_store do |store|
store.transaction do
store.keys # => [:foo, :bar, :baz]
end
end
Вызывает исключение при вызове вне блока транзакции.
# File lib/pstore.rb, line 515 def path @filename end
Возвращает строку с путём к файлу, использованному для создания хранилища:
store.path # => "flat.store"
# 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 Открывает блок транзакции для хранилища. См. раздел Транзакции.
Если аргумент read_only задан как false, блок может читать данные из хранилища и записывать в него.
Если аргумент read_only задан как true, блок не может содержать вызовы transaction, []= или delete.
Вызывает исключение при вызове внутри блока транзакции.
Закрытые методы экземпляра
# 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 имеет значение true, возвращается десериализованный Hash. Если read_only имеет значение false, возвращается кортеж из трёх элементов: десериализованный 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 Открывает указанный файл (только для чтения или для чтения и записи) и блокирует его для чтения или записи.
Возвращает открытый объект File. Если read_only имеет значение true, а файл не существует, возвращается 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–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.