時空を越えるオブジェクト 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 は 日本プログレス株式会社の製品となっているようです。