時空を越えるオブジェクト part-2
RDBとオブジェクト指向
前のアーティクル "時空を越えるオブジェクト"では、主記憶上にあるオブジェクトをファイルに落とし、そしてそれを復元するからくり "シリアライズ"について解説しました。
シリアライズによって保存されたオブジェクトはアプリケーションとは切り離されます。主記憶上に完全に復元されない限り、利用することはできません。シリアライズ(Serialize:直列化)ってくらいだから、シーケンシャルアクセスなわけで、主記憶と同じようにファイルのあちこちに飛び回って必要なデータを参照/更新できるわけではありません。
シリアライズはとても有用な機構です。が、シリアライズでは解決できない場面はいくらでもあります。早い話が主記憶に納まらないほど大量のデータを処理しなければならない場合です。
それほど大量のデータを扱わなければならないとき、僕たちがよく使うのはデータベース、特にRDB(Relational Data Base)ですわね。 RDBはひとつのデータを表現するフィールドの集合をずらーっと並べた表を複数個用意し、それらをキー項目で関係(Relation)づけて巨大なひとつの表であるかのように見せてくれます。そしてその(仮想的な)表に対して追加/削除/変更および検索といった操作をやらせてくれます。
で、このRDBとオブジェクト指向、残念ながら相性がいいとは思えないんです。RDBに格納されているひとつひとつのレコードは、それを構成する各フィールドの型とサイズそしてフィールド数はデータベース設計時に固定されます。たとえば電話帳を作ることを考えたとき、レコードは、
- 名前・文字列・32桁
- 電話番号・文字列・11桁
なんてなフィールドが定義されることでしょう。
僕はC++屋ですから、
class Person { char* name_; char* phone_; public: const char* getName() const; void setName(const char*); const char* getPhone() const; void setPhone(const char*); ... };
のようなクラスを用意し、データベース上のレコードをPerson
のインスタンスとして扱いたいのです。
すると当然の事ながら、Person
のインスタンスとデータベース上のレコードとのマッピングすなわち、
new Person
–> データベースへレコードを追加delete Person
–> データベースからレコードを削除Person::getName()
–> データベース上のレコードを参照Person::setName()
–> データベース上のレコードを更新
して欲しくなり、Person
に対する各メソッドの呼び出しに応じたデータベースの操作を行なわねばなりません。
さらに僕はこんなこともやりたくなります。
class Programmer : public Person { public: const char* getLanguage() const; void setLanguage(const char*); ... };
Person
の属性である"名前"と"電話番号"に"得意な言語"を加えたProgrammer
を定義し、電話帳の中にPerson
とProgrammer
とを一緒に登録したいのです。RDBではこんな要求を解決してくれるのかしら…
ObjectStore PSE
ここに紹介するPSE(Persistent Storage Engine)はObject Design社のオブジェクト指向データベース"ObjectStore"からオブジェクトの永続メカニズムを取り出したライブラリで、Visual C++版をオブジェクト・デザイン・ジャパンからダウンロードできます [1]。
RDB屋さんにはPSEがデータベースとはとても思えないでしょう。
PSEはアプリケーションに"不揮発性メモリ"を使わせてくれるライブラリだと考えてもらってかまいません。つまり、オブジェクトを"不揮発性メモリ"上に置くことにより、アプリケーションを終了しようがマシンの電源を落とそうが、何度でも復元できるのです(もちろん僕らのマシンにはそんな不揮発性メモリなんか積んではいませんから、ファイルに保存されたメモリ・イメージを"仮想的"不揮発性メモリとして使わせてくれるのですが)。
ビルトイン型の永続化
ではまず簡単な例から。
#include <iostream> using namespase std; int main() { int array = new int[10]; int i; // write for ( i = 0; i < 10; ++i ) { array[i] = i * i; } // read for ( i = 0; i < 10; ++i ) { cout << array[i] << ' '; } cout << endl; delete[] array; return 0; }
このコードに現れるint
配列array
をPSEで永続化しましょう。
#include <os_pse/ostore.hh> #include <iostream> #include <cstdio> using namespace std; int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); remove("array.db"); os_database* db = os_database::create("array.db"); // 1 int* array = new (db, os_ts<int>::get(), 10) int[10]; // 2 for ( int i = 0; i < 10; ++i ) { // 3 array[i] = i * i; cout << array[i] << ' '; } cout << endl; os_database_root* db_root = db->create_root("array"); // 4 db_root->set_value(array); // 5 db->save(); // 6 cout << "wrote." << endl; db->close(); // 7 OS_PSE_END_FAULT_HANDLER return 0; }
- データベース
"array.db"
を生成します。 -
int
配列をデータベース上に作成します。
new
に続く()
内の引数にはそれぞれ、- データベース
- 型情報
- 要素数(1のときは省略可)
を与えます。この例では10個の
int
をデータベース上に作成しています。 array
の各要素に値をセットします。array
をデータベースから読み込むための手掛かりとなるdb_root
を作ります。db_root
は文字列で識別され、ひとつのデータベースにいくつものdb_root
を作ることができます。db_root
にarray
をセットします。- データベースを
save()
することで、array
の内容がデータベースに固定されます。 - データベースをクローズします。
これで書き込みは完了です。作成されたデータベース "array.db"
を別のアプリケーションで読み出しましょう。
#include <os_pse/ostore.hh> #include <iostream> using namespace std; int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); os_database* db = os_database::open("array.db"); // 1 os_database_root* db_root = db->find_root("array"); // 2 int* array = static_cast<int*>(db_root->get_value());// 3 for ( int i = 0; i < 10; ++i ) { // 4 cout << array[i] << ' '; } cout << endl; db->close(); // 5 OS_PSE_END_FAULT_HANDLER return 0; }
- データベースをオープンします。
- 書き込み時に作成した
db_root
を見つけ、 - 格納された
int
配列をたぐり寄せます。 array
の内容を出力します。- データベースをクローズします。
書き込み/読み込み時、array
の各要素にアクセスしている部分に注目してください。通常の配列要素のアクセスとまったく同じです。データベースにアクセスしているとは思えないでしょう?
PSEのスゴいところは"迫真の言語透過性"なんですね。
クラスの永続化
では次にユーザ定義型(クラス)の永続化です。
/* --- point.h --- */ #ifndef __POINT_H__ #define __POINT_H__ #include <iostream> class Point { int x_; int y_; public: explicit Point(int x =0, int y =0) : x_(x), y_(y) {} friend std::ostream& operator<<(std::ostream& s, const Point p) { return s << '(' << p.x_ << ',' << p.y_ << ')'; } }; #endif
このクラスPoint
を永続化しましょう。
/* --- write.cpp --- */ #include <os_pse/ostore.hh> #include <iostream> #include <cstdio> #include "point.h" using namespace std; int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); remove("point.db"); os_database* db = os_database::create("point.db"); Point* array = new (db, os_ts<Point>::get(), 10) Point[10]; for ( int i = 0; i < 10; ++i ) { array[i] = Point(i,-i); cout << array[i] << ' '; } cout << endl; os_database_root* db_root = db->create_root("point"); db_root->set_value(array); db->save(); cout << "wrote." << endl; db->close(); OS_PSE_END_FAULT_HANDLER return 0; }
/* --- read.cpp --- */ #include <os_pse/ostore.hh> #include <iostream> #include "point.h" using namespace std; int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); os_database* db = os_database::open("point.db"); os_database_root* db_root = db->find_root("point"); Point* array = static_cast<Point*>(db_root->get_value()); for ( int i = 0; i < 10; ++i ) { cout << array[i] << ' '; } cout << endl; db->close(); OS_PSE_END_FAULT_HANDLER return 0; }
クラスを永続化するときは、 PSEに対してあらかじめ永続化するクラスの型情報を教えておかなければなりません。
型情報を登録するには、スキーマ(schema:概要/輪郭)を記述します。
/* --- schema.scm --- */ #include <os_pse/ostore.hh> #include "point.h" OS_MARK_SCHEMA_TYPE(Point)
というスキーマ定義ファイルを作成し、PSEに付属するユーティリティpssg
に食わせます。
pssg -asof schems.obj schema.scm
すると、永続オブジェクトの型情報schema.obj
が生成されます。これを一緒にリンクしてアプリケーションを作ります。スキーマ定義ファイルには、永続化したいクラスの数だけ
OS_MARK_SCHEMA_TYPE(クラス名)
を列挙します。
ポインタの永続化
冒頭に示したクラスPerson
の永続化を試みましょう。
class Person { char* name_; char* phone_; public: ... };
クラスのメンバ変数にポインタを含むとき、その内容もデータベース内に確保しなければなりません。
/* --- person.h --- */ #ifndef __PERSON_H__ #define __PERSON_H__ #include <iostream> class Person { char* name_; char* phone_; char* dup(const char* str); public: explicit Person(const char* name ="", const char* phone =""); Person& operator=(const Person&); ‾Person(); friend std::ostream& operator<<(std::ostream& s, const Person& p) { return s << '(' << p.name_ << ',' << p.phone_ << ')'; } }; #endif
/* --- person.cpp --- */ #include <os_pse/ostore.hh> #include <cstring> #include "person.h" Person::Person(const char* name, const char* phone) { name_ = dup(name); phone_ = dup(phone); } Person& Person::operator=(const Person& p) { delete[] name_; name_ = dup(p.name_); delete[] phone_; phone_ = dup(p.phone_); return *this; } Person::‾Person() { delete[] name_; delete[] phone_; } char* Person::dup(const char* str) { int l = strlen(str) + 1; char* p = new(os_database::of(this), os_ts<char>::get(), l) char[l]; strcpy(p,str); return p; }
メソッドdup()
内にあるnew()
の第一引数os_database::of(this)
は、this
がデータベース上に確保されているならそのデータベースを、そうでないならヒープを表すtransient
データベースを返します。
したがって、
Person* person = new(db, os_ts<Person>::get()) Person("police","110");
ならばメンバname_,phone_
はdb
上に、
Person* person = new Person("police","110");
ならばヒープ上に確保されます。うまくできているものです。
さて、それではここまでのまとめとして、
なる関係を持つPerson*[],Link<Person>,Stack<Person>
を永続化しましょう。ソースは少々長くなるので書き込み/読み込みのmain
のみを示します。全ソースは。
/* --- write.cpp --- */ #include <os_pse/ostore.hh> #include <iostream> #include <cstdio> #include "person.h" #include "programmer.h" #include "grade.h" #include "stack.h" using namespace std; // os_ts<X*> を作る... OS_MARK_SCHEMA_PTR_TYPE(Person*) #define db_new(type,size) new(db,os_ts< type >::get(),size) int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); remove("person.db"); os_database* db = os_database::create("person.db"); os_database_root* db_root; db_root = db->create_root("stack"); Stack<Person>* stack = db_new(Stack<Person>,1) Stack<Person>; db_root->set_value(stack); Link<Person>* link = 0; Person** array = db_new(Person*,3) Person*[3]; db_root = db->create_root("array"); db_root->set_value(array); Person* person; person = db_new(Person,1) Person("Ann",25); person->print(); array[0] = person; stack->push(person); link = db_new(Link<Person>,1) Link<Person>(person,link); person = db_new(Programmer,1) Programmer("Bob",30,"BASIC"); person->print(); array[1] = person; stack->push(person); link = db_new(Link<Person>,1) Link<Person>(person,link); person = db_new(GradedProgrammer,1) GradedProgrammer("Charlie",30,"C++",3); person->print(); array[2] = person; stack->push(person); link = db_new(Link<Person>,1) Link<Person>(person,link); db_root = db->create_root("link"); db_root->set_value(link); cout << endl; db->save(); cout << "wrote." << endl; db->close(); OS_PSE_END_FAULT_HANDLER return 0; }
/* --- read.cpp ---*/ #include <os_pse/ostore.hh> #include <iostream> #include "person.h" #include "programmer.h" #include "grade.h" #include "stack.h" using namespace std; int main() { OS_PSE_ESTABLISH_FAULT_HANDLER objectstore::initialize(); os_database* db = os_database::open("person.db"); os_database_root* db_root; Person* person; Stack<Person>* stack; cout << "array: "; db_root = db->find_root("array"); Person** array = static_cast<Person**>(db_root->get_value()); for ( int i = 0; i < 3; ++i ) { array[i]->print(); } cout << endl; cout << "stack: "; db_root = db->find_root("stack"); stack = static_cast<Stack<Person>*>(db_root->get_value()); while ( !stack->empty() ) { person = stack->top(); person->print(); stack->pop(); } cout << endl; cout << "link: "; db_root = db->find_root("link"); Link<Person>* link = static_cast<Link<Person>*>(db_root->get_value()); while ( link ) { link->data->print(); link = link->next; } cout << endl; db->close(); OS_PSE_END_FAULT_HANDLER return 0; }
PSEのダウンロードファイルには、ライブラリ、ヘッダのほか、ドキュメント群および各種サンプルが詰まっています。
RDBがしっくりいかないアナタ、ぜひPSEで遊んでみてください。
CORBAやCOMと組み合わせれば、分散オブジェクトデータベースによるちょいとしたMulti-tierアプリケーションが作れますよ!
編集部注
- ^ 2010年2月現在、ObjectStore は 日本プログレス株式会社の製品となっているようです。