YAML::Store
Video transcript & code
In the last episode we were introduced to PStore
, a simple file-based transactional persistence tool from Ruby's standard library.
We used it to store entries in a TODO list to disk. It works great for this purpose. But one day after making some changes to the Lister
library, we discover our TODO list tasks aren't being saved properly.
require "./lister"
lister = Lister.new
lister.add_list("ship chores") do |list|
list.add_task("Early rise (3:00PM)")
list.add_task("Waffles vindaloo with Kryten")
list.add_task("Call Rimmer a smeghead")
end
require "pstore"
store = PStore.new("todo.pstore")
store.transaction do |s|
s["lister"] = lister
end
require "./seed"
store = PStore.new("todo.pstore")
store.transaction do
store["lister"].lists.first.tasks # => []
end
In order to debug the problem, we first want to check if the tasks are being written out to disk. So we take a look at the contents of the todo.pstore file. And what we see is gobbledygook.
This file is in Ruby's binary marshaling format. Which means it is very efficient for Ruby to read it in and write it out. Unfortunately, it's not so easy for us humans to interpret.
Wouldn't it be nice if we could have all the convenience of PStore
, but serialized in a human-readable format like YAML? Well, as you might have guessed from the title of this article, we can have our cake and eat it too. By requiring the yaml/store
library, and using YAML::Store
everywhere we used PStore
previously, we can store our TODO list to a YAML file instead. These are the only changes we need to make; YAML::Store
has the exact same API as PStore
, so it is completely interchangeable.
require "./lister"
lister = Lister.new
lister.add_list("ship chores") do |list|
list.add_task("Early rise (3:00PM)")
list.add_task("Waffles vindaloo with Kryten")
list.add_task("Call Rimmer a smeghead")
end
require "yaml/store"
store = YAML::Store.new("todo.yaml")
store.transaction do |s|
s["lister"] = lister
end
store = YAML::Store.new("todo.yaml")
store.transaction do
store["lister"].lists.first.tasks # => []
end
Let's take a look at the todo.yaml
file.
$ cat todo.yaml --- lister: !ruby/object:Lister lists: - !ruby/struct:Lister::List name: ship chores tasks: []
Sure enough, the tasks aren't being written out. We revisit the lister
library, and realize we must have accidentally deleted the line that yields to a block given to #add_list
.
class Lister
List = Struct.new(:name, :tasks) do
def initialize(name)
super(name, [])
end
def add_task(name)
task = Task.new(name)
yield task if block_given?
tasks << task
task
end
end
Task = Struct.new(:name, :status) do
def initialize(name)
super(name, :todo)
end
end
attr_reader :lists
def initialize
@lists = []
end
def add_list(name)
list = List.new(name)
yield list if block_given?
lists << list
list
end
end
When we fix this and re-run our test script, we can see that the tasks are now saved.
require "./lister2"
lister = Lister.new
lister.add_list("ship chores") do |list|
list.add_task("Early rise (3:00PM)")
list.add_task("Waffles vindaloo with Kryten")
list.add_task("Call Rimmer a smeghead")
end
require "yaml/store"
store = YAML::Store.new("todo.yaml")
store.transaction do |s|
s["lister"] = lister
end
store = YAML::Store.new("todo.yaml")
store.transaction do
store["lister"].lists.first.tasks
# => [#<struct Lister::Task name="Early rise (3:00PM)", status=:todo>,
# #<struct Lister::Task name="Waffles vindaloo with Kryten", status=:todo>,
# #<struct Lister::Task name="Call Rimmer a smeghead", status=:todo>]
end
And when we dump the YAML file again, we can see that they are included.
$ cat todo.yaml --- lister: !ruby/object:Lister lists: - !ruby/struct:Lister::List name: ship chores tasks: - !ruby/struct:Lister::Task name: Early rise (3:00PM) status: :todo - !ruby/struct:Lister::Task name: Waffles vindaloo with Kryten status: :todo - !ruby/struct:Lister::Task name: Call Rimmer a smeghead status: :todo
So is there any reason not to use YAML::Store
? Well, YAML is easy for us humans to read, but Ruby can't parse and dump it as fast as it can read and write binary. As a result, in some very rudimentary benchmarking I've performed, YAML::Store
has turned out to be around an order of magnitude slower than PStore
. So my advice is this: use YAML::Store
by preference; but if you find it too slow for your needs, drop in PStore
as an optimization. Happy hacking!
require 'benchmark'
require 'yaml/store'
require 'fileutils'
FileUtils.rm_f('bm_blog.yml')
FileUtils.rm_f('bm_blog.pstore')
repo = YAML::Store.new('bm_blog.yml')
Post = Struct.new(:title,:body)
pstore_repo = PStore.new('bm_blog.pstore')
Benchmark.bm do |b|
b.report('yaml') do
1000.times do |i|
repo.transaction do |r|
(r["posts"] ||= []) << Post.new("Post #{i}")
end
end
end
b.report('pstore') do
1000.times do |i|
pstore_repo.transaction do |r|
(r["posts"] ||= []) << Post.new("Post #{i}")
end
end
end
end
Responses