標準添付ライブラリ紹介 【第 9 回】 PStore
初稿:2006-09-20
書いた人:西山
はじめに
Ruby には便利な標準添付ライブラリがたくさんありますが、なかなか知られていないのが現状です。そこで、この連載では Ruby の標準添付ライブラリを紹介していきます。
今回は、データを外部ファイルに保存するためのクラス PStore と PStore を継承したクラスについて紹介します。
PStore
PStore を使うと Ruby のオブジェクトを手軽に外部ファイルに保存することが出来ます。
保存されるデータファイルの内容は Marshal されたバイナリになります。
基本的な使い方
使い方は基本的には Hash のように PStore#[]= を使って値を保存して、PStore#[] を使って値を取り出します。
PStore#roots は Hash#keys に、PStore#root? は Hash#key に相当します。
ただし、transaction 中でしかデータを操作することは出来ません。
データを参照するだけの transaction の場合は引数に true を指定することで、読み込み専用の transaction にすることが出来ます。
require 'pstore'
db = PStore.new('/tmp/foo')
db.transaction do
p db.roots
ary = db['root'] = [1,2,3,4] # 配列を db に設定
ary[0] = [1,1.5] # 破壊的に変更
end # 保存は transaction を抜けるときなので変更された結果が保存される
db.transaction(true) do
p db.root?('root')
p db['root']
end
begin
db.transaction(true) do
db['root'] = 'hoge' # 書き込もうとすると PStore::Error
end
rescue PStore::Error
p $!
end
実行例 (1 回目):
[]
true
[[1, 1.5], 2, 3, 4]
#<PStore::Error: in read-only transaction>
実行例 (2 回目以降):
["root"]
true
[[1, 1.5], 2, 3, 4]
#<PStore::Error: in read-only transaction>
他に PStore#fetch と PStore#delete が Hash の同名のメソッドと同じ機能を持っています。
require 'pstore'
db = PStore.new('/tmp/foo')
db.transaction do
ary = db.fetch(:ary, [])
p ary
ary.push(0, 1, 2)
db[:ary] = ary
end
db.transaction do
ary = db.fetch(:ary, [])
p ary
db.delete(:ary)
end
実行例:
[]
[0, 1, 2]
PStore#abort と PStore#commit で、その transaction での PStore への変更を破棄したり、即座に変更を反映して transaction を抜けたり出来ます。
transaction の中で例外が発生した場合も abort と同様に変更は保存されずに transaction を抜けます。
PStore#path は、PStore のデータファイルのパスを返します。
require 'pstore'
db = PStore.new('/tmp/foo')
db.transaction do
db['foo'] = 'bar'
p [1, db['foo']]
db.abort
p [2, db['foo']] # abort したので実行されない
end
db.transaction do
db.delete('foo')
p [3, db['foo']]
db.commit
p [4, db['foo']] # commit したので実行されない
db['foo'] = 'bar' # 実行されないので delete されたまま
end
db.transaction(true) do
p [5, db['foo']]
end
p db.path
実行例:
[1, "bar"]
[3, nil]
[5, nil]
"/tmp/foo"
PStore の特徴
内部で Marshal を使っているため、以下の特徴があります。
データファイルの内容はバイナリになるため、エディタなどでデータファイルの内容を直接編集することは出来ません。
データファイルが壊れてしまった場合は、バックアップファイルがある場合はそのファイルを試すことが出来ますが、それも壊れていた場合は復旧はほぼ不可能です。
IO や Proc などの Marshal.dump が出来ないオブジェクトは保存することが出来ませんが、Marshal.dump 出来るオブジェクトなら何でも保存できて、Marshal.load 出来るものは何でも読み込めます。
Marshal::MAJOR_VERSION が違う Ruby で保存された PStore のデータファイルは扱えません。現在の Marshal::MAJOR_VERSION は 4 で Ruby 1.0 の頃から変わっていないため、将来 Marshal::MAJOR_VERSION があがるまで気にする必要はないでしょう。
Marshal::MINOR_VERSION が小さいバージョンの Ruby で保存された PStore のデータファイルを Marshal::MINOR_VERSION が大きいバージョンの Ruby で読み込むことは出来ますが、逆に大きい Marshal::MINOR_VERSION のデータファイルは読み込めません。Ruby インタープリタをバージョンダウンしたり、他の環境へ PStore のデータファイルを持って行ったりすることがない限り、気にする必要はないでしょう。
Windows 上での注意事項
ruby 1.8.5 以前の pstore.rb にはバイナリモードに関するバグ があるため、以下のスクリプトを実行して「found pstore.rb bug!」と表示された場合は修正済みの pstore.rb をダウンロード して使ってください。
require 'pstore'
db = PStore.new('bugcheck')
begin
db.transaction do
db['bugcheck'] = "\x1a"
end
db.transaction do
if db['bugcheck'] == "\x1a"
puts "OK"
end
end
rescue ArgumentError
puts "found pstore.rb bug!"
end
YAML::Store
YAML::Store は PStore と同じように Ruby のオブジェクトを YAML 形式で外部ファイルに保存します。
保存されるデータファイルの内容は YAML 形式のテキストで基本的に UTF-8N (BOM なしの UTF-8) になります。
基本的な使い方
YAML::Store の使い方は基本的に PStore と全く同じです。
プログラマーのための YAML 入門 (中級編) も参考にしてください。
require 'yaml/store'
db = YAML::Store.new('/tmp/foo.yaml')
db.transaction do
db['foo'] = 'bar'
end
db.transaction(true) do
p db['foo']
end
実行例:
"bar"
実行後の /tmp/foo.yaml の例:
---
foo: bar
YAML::Store には Hash でオプションを指定することも出来ます。
オプションの詳細は
http://yaml4r.sourceforge.net/doc/page/the_options_hash.htm
を参照してください。
require 'yaml/store'
db = YAML::Store.new('/tmp/foo.yaml', :SortKeys => true)
db.transaction do
db['foo'] = 'bar'
db['hoge'] = 'fuga'
end
db.transaction(true) do
p db.roots
end
実行例:
["hoge", "foo"]
実行後の /tmp/foo.yaml の例:
---
hoge: fuga
foo: bar
YAML::Store の特徴
内部で YAML を使っているため、以下の特徴があります。
YAML はテキスト形式のため、データファイルをテキストエディタで直接編集できます。
YAML は UTF-8 のテキスト形式のため、UTF-8 ではない String を YAML.dump すると、バイナリ扱いされて base64 などで格納される可能性があります。([ruby-list:42204] あたりの話によると、Ruby 1.8.3 からの仕様変更のようです。)
YAML.dump が出来るのに YAML.load が出来ないオブジェクトを格納してしまうとデータファイルを読み込めなくなります。そういう状態になってしまった場合は、テキストエディタでデータファイルを直接編集して原因となるオブジェクトを表す部分を削除するなどの対処が必要になります。
読み込めなくなる例
Proc のように YAML.dump が出来るのに YAML.load が出来ないオブジェクトを格納してしまうと、以下の例のように transaction に入ることが出来なくなるので注意しましょう。
require 'yaml/store'
db = YAML::Store.new('/tmp/proc.yaml')
begin
db.transaction do
db["proc"] = proc{}
end
db.transaction(true) {}
rescue TypeError
p $!
end
実行例:
#<TypeError: allocator undefined for Proc>
実行後の /tmp/proc.yaml の例:
---
proc: !ruby/object:Proc {}
独自形式の Store の作成
Marshal や YAML のように dump と load が出来る機能があれば、PStore を継承して他の形式の Store を簡単に作成することが出来ます。
オーバーライドすべきメソッド
最低限以下のメソッドをオーバーライドすれば独自形式の Store を作ることが出来ます。
継承により transaction の処理などは PStore の機能をそのまま使うことが出来ます。
initialize(filename)
initialize は YAML::Store のように別途オプションを受け取るなど、ファイル名以外を受け取る必要がなければ定義する必要はありません。
dump(table)
dump は Marshal.dump に相当するメソッドを定義します。
load(content)
load は文字列を引数とする Marshal.load に相当するメソッドを定義します。
load_file(file)
load_file は File オブジェクトを引数とする Marshal.load に相当するメソッドを定義します。(が実際には使われていないので不要です。)
XMLStore
ここでは例として、SOAP::Marshal を使って作った XMLStore を紹介します。
内容は短いのでここに全文を載せます。
xmlstore.rb
#
# XMLStore
#
# Copyright (c) 2005 Kazuhiro NISHIYAMA.
# You can redistribute it and/or modify it under the same terms as Ruby.
#
# $Id: xmlstore.rb,v 1.1 2005/03/09 13:58:25 znz Exp $
require 'pstore'
require 'soap/marshal'
class XMLStore < PStore
def initialize ( filename )
super ( filename )
end
def dump ( table )
SOAP :: Marshal . dump ( table )
end
def load ( content )
SOAP :: Marshal . load ( content )
end
def load_file ( file )
SOAP :: Marshal . load ( File . open ( file , "rb" ){ | f | f . read })
end
end
使い方は PStore や YAML::Store と同じです。
require 'xmlstore'
db = XMLStore.new('/tmp/foo.xml')
db.transaction do
db['foo'] = 'bar'
db['hoge'] = 'fuga'
end
db.transaction(true) do
p db.roots
end
実行例:
["hoge", "foo"]
実行後の /tmp/foo.xml の例:
<?xml version="1.0" encoding="utf-8" ?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body>
<Hash xmlns:n1="http://xml.apache.org/xml-soap"
xsi:type="n1:Map"
env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<item>
<key xsi:type="xsd:string">hoge</key>
<value xsi:type="xsd:string">fuga</value>
</item>
<item>
<key xsi:type="xsd:string">foo</key>
<value xsi:type="xsd:string">bar</value>
</item>
</Hash>
</env:Body>
</env:Envelope>
JsonStore
もう一つの例として、ActiveSupport の to_json を使って作ってみた JsonStore を紹介します。
これも短いので全体を載せておきます。rubygems で ActiveSupport がインストールされていることを想定しています。
YAML is JSON に書いてあるように、JSON は YAML の制限をきつくしたものと見なせるので、load には YAML.load を使っています。
require 'pstore'
require 'yaml'
require 'rubygems'
require 'active_support'
class JsonStore < PStore
def initialize(filename)
super(filename)
end
def dump(table)
table.to_json
end
def load(content)
YAML.load(content)
end
end
to_json は Symbol が文字列になるなど、JavaScript の仕様にあわせているため、load しても元通りにならないので、JsonStore は実用には向かないと思いますが、このように簡単に独自形式の Store が作れるという例として参考にしてください。
おわりに
今回は PStore を中心に PStore を継承して独自形式の Store を作成する方法までを紹介しました。
自作のアプリケーションでの手軽なデータ保存に活用していただければ幸いです。
関連リンク
著者について
西山和広。
Ruby hotlinks 五月雨版 や
現在の Ruby リファレンスマニュアル のメンテナをやっています。
Ruby リファレンスマニュアル はいつでも執筆者募集中 です。
何かあれば、マニュアル執筆編集に関する議論をするためのメーリングリスト rubyist@freeml.com (参加方法 ) へどうぞ。
Ruby リファレンスマニュアルは現在青木さんによる新システムに移行準備中です。
手伝っていただける方は ruby-reference-manual ML に入ってください。
標準添付ライブラリ紹介 連載一覧