PRb
PRbは、永続化できるRubyオブジェクトを目指して開発をはじめました。 PRbはRubyのオブジェクト空間をRDBMSに写像するOODB風なものの習作です。 PRbの現在のバージョンはDBI/DBD/Pgを経由してPostgreSQLを利用します。
APIはまだまだ変更されます。
News
- PRbLight - SQLiteを用いたバージョンを設計し直してみようっと。
PRbとは
PRbは、永続化できるRubyオブジェクトを目指して開発をはじめました。 PRbはRubyのオブジェクト空間をRDBMSに写像するOODB風なものの習作です。 PRbの現在のバージョンはDBI/DBD/Pgを経由してPostgreSQLを利用します。 内部ではできるだけ標準的なSQLを使用していますが、現在はPostgreSQLだけで動作します。
完全にRubyオブジェクトを永続化することを狙っていましたが、 Rubyそっくりなものを作るのはちょっと難しいことに気付きました。 そこで徹底的に永続化できるRubyのオブジェクトに似ているオブジェクト、 に方針を変更しました。
PRbはRDBMSのテーブルをオブジェクトのメモリ空間として使用するOODBです。 全てのメソッドはトランザクションに対応しています。 一連のメソッド呼出しをロールバックすることができますし、トランザクション中に 他のプロセスがオブジェクトを操作してしまうこともありません。
PRbが永続化するのはHashやArray、インスタンス変数などオブジェクトの関係です。 クラス定義などを保存することはできません。
PRbの世界観
PRbの世界では全てのオブジェクトはいずれかのクラスに属し、それぞれ固有の識別子(id) をもっています。 PRbのオブジェクトはidそのものがオブジェクトの値を示すimmediateオブジェクトと それ以外の一般的なオブジェクトに分類されます。
immidiateオブジェクトはRubyと同様にFixnumやtrue, false, nil, Symbolなどです。 これらは属性をもちません。 一般的なオブジェクトは属性をもちます。属性は内部的で連想配列(alist)として扱われます。 インスタンス変数や配列、ハッシュなど、これらはどれも連想配列で実現されています。 RubyのHashはキーになるオブジェクトに制限がないため、高速な実装ができないと 想像しています。
PRbが保存するのはオブジェクトの属すクラスと、連想配列で表現された属性です。 メソッドなどの振る舞いは保存されません。またクラスの継承関係も保存の対象ではありません。
PRbのインストール
必要なライブラリ等
ruby-dbiのdbi, dbd_pgが必要です。 dbd_pgにはPostgreSQL本体とRubyからpostgresを利用するためのruby-postgresが必要と なります。
PRbのインストール
PRb自体はRubyで書かれています。root権限でinstall.rbを実行します。
% tar xzvf prb-0.x.tar.gz % cd prb % sodo ruby install.rb
テストの実行
ライブラリ、RDB、データベースを利用する権限などが揃っているかどうか、 UnitTestを実行して確認します。 UnitTestにはデータベースの作成権限が必要です。 適切な設定、適切なユーザで実行してください。
% cd runit % ruby prbtest.rb
付属するツール/サンプル
ディレクトリprb/tools以下にPRbの使用するデータベースを準備するツールが 含まれています。
% ruby tools/prbinit.rb
また、ディレクトリprb/demo以下に簡単なサンプルが入っています。
PRbの前に
DBIPool
DBIPoolはマルチスレッド環境の下で使用できるDBIのコネクションのプールです。 DBIPoolの動作を確認したのDBDはDBD::Pgのみです。
PRbと独立して使用することができます。
PRbを利用するにはDBIPoolの初期化が必要です。
PRbのモジュールとクラス
PRbはDBIPoolを与えて初期化しますが、デフォルトの値で生成するユーティリティメソッドが 準備されています。
PRb
PRb.start_service(ARGV=[])
-
PRbをデフォルトの値で初期化するユーティリティメソッドです。
PRb.primary
-
PRbのデフォルトのストレージを返します。
PRb::Store
PRb::StoreはPRbの保存を管理するオブジェクトです。 RDBの一つのデータベースと関連付けられます。
root
-
根となるオブジェクトを返します。
each_object(klass, &block)
-
クラスklassの全てのオブジェクトを一つずつブロックに与えます。 全てのStringを調べる例を示します。
PRb.primary.each_object(String) do |str| p str end
クラスの継承関係は考慮されません。
PRb独自のオブジェクトとコレクション
PRbでは全てのオブジェクトは内部で連想配列として表現されます。 連想配列のキーも要素もPRbのオブジェクトです。 この特徴によって、いくつかの制限が発生します。
RubyのHashと同様に任意のオブジェクトをキーにすることが可能ですが、 取り出す際にはオブジェクトIDが一致したものしか取り出せません。 また、単純な配列も連想配列として実装されるためにArrayに対する要素の挿入は 高速に行えません。
PRb::PRbO
PRbの基本的な操作を定義したモジュールです。通常mix-inして使用します。 PRbObjectはこのモジュールをincludeしています。
transaction(&block)
-
オブジェクトをロックしてブロックを実行します。 ブロックの中で例外が発生すると、PRbオブジェクトに対して行った操作がすべて 巻き戻され、transactionを開始する前の状態の戻ります。 ブロックが正常に終了して初めてPRbオブジェクトの操作が登録されます。
PRb::PRbObject
PRbで扱うオブジェクトの基本となるクラスです。
PRbObject.prb_attr(*args)
-
クラスにprbの属性にアクセスするメソッドを定義します。 Rubyのattrと同様です。
class Foo < PRb::PRbObject prb_attr :foo end
上記のコード(prb_attr :foo)は、下記の定義と同じです。
class Foo < PRb::PRbObject def foo _get(:foo) end def foo=(value) _set(:foo, value) end end
_getはPRbObject(実際にはモジュールPRb0)のプライベートなメソッドで、 PRbの空間から属性を取り出すことができます。 _setは逆に属性の値を設定するメソッドです。
Rubyにはインスタンス変数のアクセスをフックする方法がないため、属性のアクセスは メソッド呼出しになります。
obj == other
-
メソッド==は同値を検査するメソッドです。 クラスが同じで、PRbのデータベース名と識別子が一致すれば同値と判定されます。
PRb::PRbList
PRbListは列を扱うデータ構造で、双方向キューに似た操作をもちます。 先頭または末尾に要素を追加できます。 Arrayのように任意の位置に要素を挿入することはできません。
push(v)
-
末尾に要素を追加します。
unshift(v)
-
先頭に要素を追加します。
pop
-
末尾の要素を取り出して返します。
shift
-
先頭の要素を取り出して返します。
head, tail
-
先頭、または末尾の要素を返します。
to_a
-
Arrayに変換します。
length, size
-
要素数を返します。
each
-
いつものeachです。要素を前から順にyieldします。 Enumerableはincludeしていません。(必要?)
PRb::PRbAttr
キーをシンボルに限定したHashです。
keys
-
全てのキーを返す。
[key]
-
keyに対応する要素を返す。
[key] = value
-
keyに対応する要素をセットする。
PRb::PRbRoot
PRbRootはPRbのストレージの根となるオブジェクトのために作られました。 PRbRootはPRb::PRbAttrのサブクラスで、シンボル以外にもオブジェクト自身の識別子を キーにすることができるます。
add(obj)
-
オブジェクトobjを保持します。addで追加したオブジェクトはdeleteされるまで GCから保護されます。
delete(obj)
-
オブジェクトobjの保持をやめます。
all_entry
-
全ての要素を返します。
デモ
たまごっちプラスの進化の過程と結婚などを記録する、家系図を開発します。
仕様
x* 進化の過程と結婚が記録できる。
- irbなどを使用して情報を記録する。
- 多少Rubyスクリプトを書かなくてはならないが我慢する。
データモデル
端折っていきなりデータモデルを示します。
AbstratTmgcはUMLのための擬似的な抽象クラスで、実際には定義しません。
Tmgc::Place
Placeはたまごっちプラスの本体に相当するクラスです。 各たまごっちプラスを識別するための名前と、たまごっちの集合をもちます。
# tmgc00.rb require 'prb/prb' module Tmgc class Place < PRb::PRbObject prb_attr :name, :list def initialize(name) self.name = name self.list = PRb::PRbList.new end end end PRb.start_service
PlaceはPRbObjectを継承することにしました。 一般的な属性を持つだけのオブジェクトであれば、DRbObjectで充分と思われます。 Placeの属性はnameとlistです。 それぞれたまごっちプラス本体の名前と、これまでに育てたたまごっちのリストです。 リストはPRbListを使用します。 PRbListは双方向キューのようなデータ構造です。 Placeが管理するたまごっち(Tmgc::Tmgc)の集合は、世代順に保持できれば充分なので、 PRbListで良さそうです。
実際にirbを使用してPRbのストレージにTmgc::Placeを覚えさせてみましょう。 PRbの根に :tmgc という名前にPRbAttrを置くことにします。 このPRbAttrにPlaceを登録します。
% irb --simple-prompt >> load 'tmgc00.rb' >> root = PRb.primary.root >> root.transaction do ?> root[:tmgc] = PRb::PRbAttr.new >> root[:tmgc][:seki] = Tmgc::Place.new('seki') >> end >> world = root[:tmgc]
はじめのloadによってPlaceの定義とPRbの初期化が行われます。 PRb.primaryはPRbのデフォルトのストレージを、 PRb.primary.rootはそのストレージの根となるオブジェクトを指します。 root[:tmgc]にPRbAttrをセットし、 root[:tmgc][:seki]に'seki'という名前で生成したPlaceをセットします。 これから行う実験の中心はroot[:tmgc]となるので、worldという変数でさせるように しましょう。
world[:seki]を調べます。
>> world[:seki] => #<Tmgc::Place ref=76> >> world[:seki].name => "seki"
名前が設定されていますね。リストはどうでしょう。空のリストのはずです。
>> world[:seki].list => #<PRb::PRbList ref=84> >> world[:seki].list.size => 0 >> world[:seki].list.to_a => []
一度終わらせてもう一度起動してみます。
>> exit % irb --simple-prompt >> load 'tmgc00.rb' => true >> world = PRb.primary.root[:tmgc] => #<PRb::PRbAttr ref=72> >> world[:seki] => #<Tmgc::Place ref=76> >> world[:seki].name => "seki"
先ほどの実験で設定したPlaceが残っていることがわかります。
さらにもう一つPlaceを生成してみます。名前は'reona'です。
>> world.transaction do ?> world[:reona] = Tmgc::Place.new('reona') >> end => #<Tmgc::Place ref=136>
each_objectを使って、いくつPlaceが保存されているか調べてみます。
>> PRb.primary.each_object(Tmgc::Place) {|place| p place.name} "seki" "reona" => 2
二つのTmgc::Placeが確認できました。 each_objectはクラスを指定してインスタンスをオブジェクトの一覧にアクセスするメソッドです。 GC対象の可能性があるオブジェクトも返るので注意が必要です。
次にたまごっちのキャラクターを表現するTmgc::Tmgcクラスを追加しましょう。 Tmgc::Tmgcは名前と性別(雄か否か)、種別、世代番号とイベントの履歴を持ちます。 名前と性別、親たまごっちを与えて生成します。 世代番号は親のたまごっちの世代番号から求めます。 最初のたまごっちには親はいませんので、世代番号は1とします。
# tmgc01.rb require 'prb/prb' module Tmgc class Place < PRb::PRbObject prb_attr :name, :list def initialize(name) self.name = name self.list = PRb::PRbList.new end def add(tmgc) transaction do self.list.push(tmgc) end end end class Tmgc < PRb::PRbObject prb_attr :name, :male, :kind, :generation prb_attr :history prb_attr :parent def initialize(name, is_male, parent=nil) transaction do self.name = name self.male = is_male self.parent = parent self.generation = parent ? parent.generation : 1 self.history = History.new end end end class History < PRb::PRbList end end PRb.start_service
実験します。
% irb --simple-prompt >> load 'tmgc01.rb' >> world = PRb.primary.root[:tmgc] >> seki = world[:seki] >> seki.transaction do ?> seki.add(Tmgc::Tmgc.new('すぴか', true)) >> end
すぴかという名前でTmgc::Tmgcを生成して、seki(Place)にaddします。 生成してからaddされるまでの瞬間、すぴかはPRbのストレージの中で誰からも参照されていません。 微妙な一瞬です。この瞬間にGCが走るとすぴかを失う可能性があります。 これをふせぐためにトランザクションの中で生成とaddを実行しています。 *1
each_objectを使ってすぴかが登録されているか確認します。
>> PRb.primary.each_object(Tmgc::Tmgc) {|x| p x.name} "すぴか" => 1
さすがに飽きてきそうなのでいきなり完成させます。
# tmgc.rb require 'prb/prb' module Tmgc class Place < PRb::PRbObject prb_attr :name, :list def initialize(name) self.name = name self.list = PRb::PRbList.new end def add(tmgc) self.list.push(tmgc) end def curr self.list.tail end def evolve(kind) curr.evolve(kind) end def marry(partner, baby_is_male) curr.marry(partner, baby_is_male) end def part(name) transaction do baby = curr.baby baby.name = name add(baby) end end def arranged_marry(kind, baby_is_male) transaction do tmgc = OmiaiTmgc.new(kind) marry(tmgc, baby_is_male) end end def die curr.die end end class OmiaiTmgc < PRb::PRbObject prb_attr :kind def initialize(kind) self.kind = kind end def name "OMIAI" end end class Tmgc < PRb::PRbObject prb_attr :name, :male, :kind, :generation prb_attr :history prb_attr :baby, :partner, :parent def initialize(name, is_male, parent=nil) transaction do self.name = name self.male = is_male self.parent = parent self.generation = parent ? parent.generation : 1 self.history = PRb::PRbList.new end end def evolve(kind) transaction do self.kind = kind self.history.push(kind) end end def marry(partner, baby_is_male) transaction do self.baby = Tmgc.new(nil, baby_is_male, self) self.partner = partner end end end end PRb.start_service
実装 - 以下作成中
PRbの現在のバージョンはDBI/DBD/Pgを経由でPostgreSQLを利用します。
PRbは次の3つのテーブルを使います。
- table symbol --- シンボルの表。idと文字列からなる。Rubyと同様に要素は単調増加します。
- table object --- オブジェクトの表。id, とクラス、値からなる。
- table alist --- 属性リスト。オブジェクトのidとキーのid、値のidからなる。
ここでidはRubyに似た次の規則を持ってます。
- 0 --- false
- 2 --- true
- 4 --- nil
- 奇数 --- Fixnum
- 4の倍数+2 --- Symbol
- 4の倍数 --- その他のオブジェクト
Symbol
- Symbolはずるい。検索を早くするために専用のテーブルをもちます。 テーブルは通常、単調増加で要素が増え続けます。
- 単調増加の特徴に依存してライブラリ内部でもキャッシュします。
PRbのPR
- 現バージョンでPogo相当くらいできるじゃん。たぶん。
排他制御
- 普通のオブジェクトの更新は排他制御されるよ
*1ちょっとかっこわるい