XMLを用いた永続オブジェクトの試み
永続オブジェクトとXML
近頃XMLがやたらとクローズアップされてきています。サーバ/サーバ間あるいはクライアント/サーバ間のインタフェースとしてXMLを用いることで様々な接続に対応できるってことで、e-commerceをはじめとしたITの基礎技術として脚光を浴びてきたということでしょう。(e-commerceについては僕には良くわからないし、そんなbigなシステムを構築するつもりはありません^^;)
XMLで遊んでいると様々な使いみちを思い付きます。アプリケーションの設定情報なんかをXMLで書くとか、状態遷移表をXMLで表現するとか…
XMLは階層構造を持ったデータを表現する汎用のデータ・フォーマットです。階層構造を持ったデータを表現できるのなら、オブジェクトをXMLで表現できるだろうと考えました。たとえば、
class person { string name_; // 名前 int age_; // 年齢 public: person(string n, int a) : name_(n), age_(a) {} ... };
のようなオブジェクトがあったとき、person(“adam”,24) を
<person> <name>adam</name> <age>24</age> </person>
のようなXMLに変換すること、逆にXMLからpersonを生成することができるんじゃないか…というわけ。
これができればアプリケーションが扱うオブジェクトの集合をXML化でき、XMLによるアプリケーション間でのオブジェクトのやりとりが可能になります。
…というわけで、XMLによるオブジェクトの永続化の試みです。
※このアーティクルでは、Microsoft XML Parser (MSXML.dll) を用いますが、XML4C や Xerces などXML Parserでも考え方は同じです。
書き込みメカニズム
例として
class person { string name_; // 名前 int age_; // 年齢 public: person(string n, int a) : name_(n), age_(a) {} ... };
をXML化することを考えます。そのために新たなメソッドvirtual void save(IXMLDOMNodePtr node) const
を追加します。
※IXMLDOMNodePtrはMSXMLにおけるDOMNodeのスマート・ポインタです。
メソッド save は、自分自身のメンバに基づいて DOMNode を生成します。たとえば name_ == "adam" , age_ == 24
であるpersonオブジェクトからは
<person> <name>adam</name> <age>24</age> </person>
なるDOMNodeを生成します。
次に、生成された DOMNode を引数 node の子エレメントとして追加します。たとえば node が <root/>
(あるいは<root>
</root>
) であるとすると、
<root> <person> <name>adam</name> <age>24</age> </person> </root>
となります。node が既に子エレメントを持っていた場合、その末っ子として追加します。
まず、メソッド save の冒頭では、
- [1] “person” をタグ名とする DOMElement を作る
を行ないます。
次にメンバ変数name_から<name>adam</name>
なるDOMNodeを作るには、
- [2] name_の値を文字列化し、それを値とする DOMText を作る
- [3] “age” をタグ名とする DOMElement を作る
- [4] [3]の子ノードとして[2]を追加する
というステップを踏みます。
これでふたつの DOMNode :
- [1]
<person></person>
- [4]
<name>adam</name>
ができたので、
- [5] [1]の子ノードとして[4]を追加する
を行ないます。
全く同様に age_ から<age>24</age>
なるDOMNode を作り、[1]の子ノードとして追加します。
最後に、save の引数として与えられた node の子ノードとして[1]を追加します。ここまでを実装してみましょう:
void person::save(IXMLDOMNodePtr node) const { assert(node != 0); IXMLDOMDocumentPtr doc = node->ownerDocument; IXMLDOMNodePtr elem = doc->createElement("person"); IXMLDOMNodePtr item; IXMLDOMTextPtr text; item = doc->createElement("name"); text = doc->createTextNode(name_.c_str()); item->appendChild(text); elem->appendChild(item); item = doc->createElement("age"); char buff[16]; _itoa(age_,buff,10); text = doc->createTextNode(buff); item->appendChild(text); elem->appendChild(item); node->appendChild(elem); }
personの派生クラス:
class programmer : public person { string lang_; // 言語 ... };
のメソッドsave(IXMLDOMNode node) const
では、ベースクラスのメソッドsaveを再帰的に呼び出します。すなわち、
- [6] “programmer” をタグ名とする DOMElement を作る
- [7] [6]を引数にベースクラス(person)のsaveを呼び出しことで、[6]の子ノード
<person>...</person>
を追加する - [8] メンバ変数 lang_ に基づいて
<language>...</language>
を作る - [9] [6]の子ノードとして[8]を追加する
実装は以下のようになります:
void programmer::save(IXMLDOMNodePtr node) const { assert(node != 0); IXMLDOMDocumentPtr doc = node->ownerDocument; IXMLDOMNodePtr elem = doc->createElement("programmer"); IXMLDOMNodePtr item; IXMLDOMTextPtr text; person::save(elem); item = doc->createElement("language"); text = doc->createTextNode(lang_.c_str()); item->appendChild(text); elem->appendChild(item); node->appendChild(elem); }
これによって、programmerからは以下のような DOMNode が生成されることになります:
<programmer> <person> <name>bill</name> <age>31</age> </person> <language>BASIC</language> </programmer>
最後に、XMLとしての体裁を整える、すなわち、
- [10] DOMDocument を生成する
- [11] rootエレメント を作る
- [12] [11]で作ったrootエレメントに対し、オブジェクト(person or programmer)をsaveする
という処理を行なうことでXMLによる永続データの完成です。
int save(string file) { // 空のドキュメントを生成する IXMLDOMDocumentPtr doc("MSXML.DOMDocument"); doc->appendChild( doc->createProcessingInstruction( // ルートエレメントを生成する。 IXMLDOMNodePtr root = doc->createElement("root"); doc->appendChild(root); // [1] personとprogrammerをrootにsave cout << "save person & programmer¥n"; person("adam",24).save(root); programmer("bill",20,"BASIC").save(root); // [2] vector<programmer>を生成 vector<programmer> pv; pv.push_back(programmer("bjarne",28,"C++")); pv.push_back(programmer("wirth",30,"Modula-2")); // [3] rootの子"programmers"を作り、 // その中にvector<programmer>をsave cout << "save programmers¥n"; IXMLDOMNodePtr pvelm = doc->createElement("programmers"); for ( int i = 0; i < pv.size(); ++i ) pv[i].save(pvelm); root->appendChild(pvelm); // XMLファイルを出力 doc->save(file.c_str()); return 0; }
上記のサンプルコードによって生成されたpersist.xmlをIE5でブラウズしたイメージを以下に示します:
読み込みメカニズム
読み込みは前述の書き込みと同じ順序で DOMNode をほどいてゆきます。
すなわち personのメソッド void load(IXMLDOMNodePtr node)
では、
- [1] 最初の子ノード(
<name>...</name>
)を取得する - [2] [1]の子ノード(DOMText)を取得する
- [3] [2]から得られる文字列を基にメンバ変数 name_ をセットする
- [4] 次の子ノード(
<age>...</age>
)を取得する - [5] [4]の子ノード(DOMText)を取得する
- [6] [5]から得られる文字列を基にメンバ変数 age_ をセットする
void person::load(IXMLDOMNodePtr node) { assert(node != 0); assert(node->nodeName == _bstr_t("person")); IXMLDOMNodePtr item; IXMLDOMNodePtr text; item = node->firstChild; text = item->firstChild; name_ = static_cast<const char*>(_bstr_t(text->nodeValue)); item = item->nextSibling; text = item->firstChild; age_ = atoi(static_cast<const char*>(_bstr_t(text->nodeValue))); }
同様に programmer::load(IXMLDOMNodePtr node)
では:
- [7] 最初の子ノード(
<person>...</person>
)を取得する - [8] [7]を引数にベースクラス(person)のメソッドloadを呼び出し、ベースクラスのメンバ変数をセットする
- [9] 次の子ノード(
<language>...</language>
)を取得する - [10] [9]の子ノード(DOMText)を取得する
- [11] [10]から得られる文字列を基にメンバ変数 lang_ をセットする
void programmer::load(IXMLDOMNodePtr node) { assert(node != 0); assert(node->nodeName == _bstr_t("programmer")); IXMLDOMNodePtr item; IXMLDOMNodePtr text; item = node->firstChild; person::load(item); item = item->nextSibling; text = item->firstChild; lang_ = static_cast<const char*>(_bstr_t(text->nodeValue)); }
永続データ(すなわちXML)からオブジェクトを復元するには:
- [12] XMLをパースし、DOMDocument を取得する
- [11] [12]からrootエレメントを取得する
- [12] [11]で作ったrootエレメントに対し、オブジェクト(person or programmer)をloadする
という処理を行なうことになります。
int load(string file) { IXMLDOMDocumentPtr doc("MSXML.DOMDocument"); doc->validateOnParse = VARIANT_FALSE; doc->load(file.c_str()); IXMLDOMNodePtr root = doc->documentElement; IXMLDOMNodePtr node; // save:[1] でsaveした person/programmer を読み出す cout << "¥nload person & programmer¥n"; int i; person* p; node = root->firstChild; p = node->nodeName == _bstr_t("person") ? new person : new programmer; p->load(node); p->print(cout); cout << endl; delete p; node = node->nextSibling; p = node->nodeName == _bstr_t("person") ? new person : new programmer; p->load(node); p->print(cout); cout << endl; delete p; // save:[3] でsaveしたvector<programmer>を読み出す cout << "¥nload programmers¥n"; vector<programmer> pv; node = node->nextSibling; for ( node = node->firstChild; node != 0; node = node->nextSibling ) { programmer p; p.load(node); pv.push_back(p); } for ( i = 0; i < pv.size(); ++i ) { pv[i].print(cout); cout << endl; } // saveされたpersonを出力 // このとき、programmerが内包するpersonも含む cout << "¥nload persons (including programmers)¥n"; IXMLDOMNodeListPtr nodes = doc->getElementsByTagName("person"); for ( i = 0; i < nodes->length; ++i ) { person p; p.load(nodes->item[i]); p.print(cout); cout << endl; } return 0; }
上記コードの実行結果は以下のようになります:
サンプルコード
#include <iostream> // ostream #include <string> // string #include <vector> // vector #include <cassert> // assert #include <cstdlib> // atoi, _itoa #import "msxml.dll" rename_namespace("msxml") namespace msxml { /* * テストのための2つのクラス : person と programmer */ class person { std::string name_; int age_; public: person(std::string name, int age) : name_(name), age_(age) {} person() {} virtual void save(IXMLDOMNodePtr node) const { assert(node != 0); IXMLDOMDocumentPtr doc = node->ownerDocument; IXMLDOMNodePtr elem = doc->createElement("person"); IXMLDOMNodePtr item; IXMLDOMTextPtr text; item = doc->createElement("name"); text = doc->createTextNode(name_.c_str()); item->appendChild(text); elem->appendChild(item); item = doc->createElement("age"); char buff[16]; _itoa(age_,buff,10); text = doc->createTextNode(buff); item->appendChild(text); elem->appendChild(item); node->appendChild(elem); } virtual void load(IXMLDOMNodePtr node) { assert(node != 0); assert(node->nodeName == _bstr_t("person")); IXMLDOMNodePtr item; IXMLDOMNodePtr text; item = node->firstChild; text = item->firstChild; name_ = static_cast<const char*>(_bstr_t(text->nodeValue)); item = item->nextSibling; text = item->firstChild; age_ = atoi(static_cast<const char*>(_bstr_t(text->nodeValue))); } virtual void print(std::ostream& strm) const { strm << name_ << '(' << age_ << ')'; } }; class programmer : public person { std::string lang_; public: programmer(std::string name, int age, std::string lang) : person(name,age), lang_(lang) {} programmer() {} virtual void save(IXMLDOMNodePtr node) const { assert(node != 0); IXMLDOMDocumentPtr doc = node->ownerDocument; IXMLDOMNodePtr elem = doc->createElement("programmer"); IXMLDOMNodePtr item; IXMLDOMTextPtr text; person::save(elem); item = doc->createElement("language"); text = doc->createTextNode(lang_.c_str()); item->appendChild(text); elem->appendChild(item); node->appendChild(elem); } virtual void load(IXMLDOMNodePtr node) { assert(node != 0); assert(node->nodeName == _bstr_t("programmer")); IXMLDOMNodePtr item; IXMLDOMNodePtr text; item = node->firstChild; person::load(item); item = item->nextSibling; text = item->firstChild; lang_ = static_cast<const char*>(_bstr_t(text->nodeValue)); } virtual void print(std::ostream& strm) const { person::print(strm); strm << ':' << lang_; } }; /* * お試し */ using namespace std; int save(string file) { // 空のドキュメントを生成する IXMLDOMDocumentPtr doc("MSXML.DOMDocument"); doc->appendChild( doc->createProcessingInstruction( // ルートエレメントを生成する。 IXMLDOMNodePtr root = doc->createElement("root"); doc->appendChild(root); // [1] personとprogrammerをrootにsave cout << "save person & programmer¥n"; person("adam",24).save(root); programmer("bill",20,"BASIC").save(root); // [2] vector<programmer>を生成 vector<programmer> pv; pv.push_back(programmer("bjarne",28,"C++")); pv.push_back(programmer("wirth",30,"Modula-2")); // [3] rootの子"programmers"を作り、 // その中にvector<programmer>をsave cout << "save programmers¥n"; IXMLDOMNodePtr pvelm = doc->createElement("programmers"); for ( int i = 0; i < pv.size(); ++i ) pv[i].save(pvelm); root->appendChild(pvelm); // XMLファイルを出力 doc->save(file.c_str()); return 0; } int load(string file) { IXMLDOMDocumentPtr doc("MSXML.DOMDocument"); doc->validateOnParse = VARIANT_FALSE; doc->load(file.c_str()); IXMLDOMNodePtr root = doc->documentElement; IXMLDOMNodePtr node; // [1] でsaveした person/programmer を読み出す cout << "¥nload person & programmer¥n"; int i; person* p; node = root->firstChild; p = node->nodeName == _bstr_t("person") ? new person : new programmer; p->load(node); p->print(cout); cout << endl; delete p; node = node->nextSibling; p = node->nodeName == _bstr_t("person") ? new person : new programmer; p->load(node); p->print(cout); cout << endl; delete p; // [3] でsaveしたvector<programmer>を読み出す cout << "¥nload programmers¥n"; vector<programmer> pv; node = node->nextSibling; for ( node = node->firstChild; node != 0; node = node->nextSibling ) { programmer p; p.load(node); pv.push_back(p); } for ( i = 0; i < pv.size(); ++i ) { pv[i].print(cout); cout << endl; } // saveされたpersonを出力 // このとき、programmerが内包するpersonも含む cout << "¥nload persons (including programmers)¥n"; IXMLDOMNodeListPtr nodes = doc->getElementsByTagName("person"); for ( i = 0; i < nodes->length; ++i ) { person p; p.load(nodes->item[i]); p.print(cout); cout << endl; } return 0; } int entry(string file) { int result; if ( result = save(file) ) return result; if ( result = load(file) ) return result; return result; } } using namespace msxml; int main(int argc, char* argv[]) { if ( FAILED(CoInitialize(0)) ) return 1; int result = entry("persist.xml"); CoUninitialize(); return result; }