株式会社エス・スリー・フォー

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でブラウズしたイメージを以下に示します:

persist.xml

読み込みメカニズム

読み込みは前述の書き込みと同じ順序で 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;
}