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スクリプトを書かなくてはならないが我慢する。

データモデル

端折っていきなりデータモデルを示します。

http://www2a.biglobe.ne.jp/%7eseki/ruby/tp/tmgc.jpg

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の倍数 --- その他のオブジェクト

http://www2a.biglobe.ne.jp/%7eseki/ruby/prb.jpg

Symbol

  • Symbolはずるい。検索を早くするために専用のテーブルをもちます。 テーブルは通常、単調増加で要素が増え続けます。
  • 単調増加の特徴に依存してライブラリ内部でもキャッシュします。

PRbのPR

  • 現バージョンでPogo相当くらいできるじゃん。たぶん。

排他制御

  • 普通のオブジェクトの更新は排他制御されるよ

*1ちょっとかっこわるい