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

データベースにコレクションを格納する

はじめに

Rogue Wave SourcePro Core:Essential Tools Module(旧Tools.h++)が提供するコレクション・クラスの多くは、vectorやlistなどの標準C++ライブラリのコンテナにはない機能である永続化(Persistency)をサポートしており、Rogue Wave コレクションの大きなAdvantageのひとつです。永続化を利用することで、コレクションを丸ごとファイルに保存し、そして復元することができます:

save (保存)
RWTValDlist<RWCString> str_list;
RWFile file("persist.dat");
file << str_list;
restore (復元)
RWTValDlist<RWCString> str_list;
RWFile file("persist.dat");
file >> str_list;

このように、コレクションに格納される要素が永続化できるなら、コレクションを丸ごとファイルに保存し、そして復元できます。

上の例では永続化をサポートしたクラスRWCStringのコレクションRWTValDlist<RWCString>の永続化を行なっています。ユーザ定義クラスに永続化をサポートさせるには、例えば以下のようなコードとなります。

IntAndStr.h

#ifndef __INTANDSTR_H__
#define __INTANDSTR_H__

#include <iostream>     // ostream
#include <rw/cstring.h> // RWCString
#include <rw/edefs.h>   // RWDECLARE_PERSISTABLE

/*
 * ユーザ定義型
 */
class IntAndStr {
  int       num_;
  RWCString str_;
public:
  explicit IntAndStr(int n, const char* s) : num_(n), str_(s) {}
  IntAndStr() {}

  friend std::ostream& operator<<(std::ostream&, const IntAndStr&);

  friend void rwSaveGuts(RWvostream&, const IntAndStr& ias);
  friend void rwSaveGuts(RWFile&    , const IntAndStr& ias);
  friend void rwRestoreGuts(RWvistream&, IntAndStr& ias);
  friend void rwRestoreGuts(RWFile&    , IntAndStr& ias);
};

RWDECLARE_PERSISTABLE(IntAndStr)

#endif

IntAndStr.cpp

#include <rw/epersist.h> // RWDEFINE_PERSISTABLE
#include "IntandStr.h"

void rwSaveGuts(RWvostream& strm, const IntAndStr& ias)
  { strm << ias.num_ << ias.str_; }

void rwSaveGuts(RWFile& file, const IntAndStr& ias)
  { file << ias.num_ << ias.str_; }

void rwRestoreGuts(RWvistream& strm, IntAndStr& ias)
  { strm >> ias.num_ >> ias.str_; }

void rwRestoreGuts(RWFile& file, IntAndStr& ias)
  { file >> ias.num_ >> ias.str_; }

std::ostream& operator<<(std::ostream& strm, const IntAndStr& ias)
  { return strm << ias.num_ << '/' << ias.str_; }

RWDEFINE_PERSISTABLE(IntAndStr)

それでは、RDB(Relational Database)にコレクションを格納することはできるのでしょうか。すなわち、RDBが管理するテーブルのカラムのひとつにコレクションを押し込むのです。

テーブルのカラムとして許されているデータ型は基本的には数値もしくは文字列に限られています。しかしながら幸いにも一般によく使われているRDBの多くはデータ型として'BLOB'を許しています。BLOB(Binary Large Object)は任意のバイト列を表現するデータ型で、音声や静止画/動画などをRDBに格納するのに用いられます。

したがって、コレクションからバイト列への変換およびバイト列からコレクションへの逆変換ができるなら、コレクションをBLOBとしてRDBに格納することができるでしょう。

バイト列への永続化

SourceProを用いてオブジェクトをファイルに永続化するには2つの方法があります。ひとつは前述のRWFileによる方法。もうひとつは仮想ストリームRWvostream/RWvistreamによるものです。

save (保存)
RWTValDlist<RWCString> str_list;
std::ofstream file("persist.dat",std::ios::binary);
RWbostream strm(file)
strm << str_list;
restore (復元)
RWTValDlist<RWCString> str_list;
std::ifstream file("persist.dat",std::ios::binary);
RWbistream strm(file)
strm >> str_list;

この例ではバイナリ・ファイル"persist.dat"からstd::ofstream/std::ifstream、さらにRWbostream/RWbistream(それぞれRWvostream/RWvistreamの派生クラス)を生成し、ファイルを媒体とした永続性を実現しています。これをヒントにバイト列を媒体とした永続化を試みます。

RWbostream/RWbistreamのコンストラクタにはstd::ostream/std::istreamだけでなく、std::streambufを与えることができます。そこでstd::streambufから導出されたstd::stringbufをRWbostream/RWbistreamのコンストラクタに与えることで、文字列std::stringを永続化の媒体とすることができます。std::stringはC/C++で通常用いられる文字列char[]と異なり、文字列を構成する文字として'¥0'を含む任意のバイトを許します。つまりstd::stringは実質的にいわゆる'文字列'としてだけではなく、任意の'バイト列'として扱うことができます。

コレクション -> バイト列
std::string bytes;
RWTValDlist<RWCString> collection;
std::stringbuf buffer;
RWbostream stream(buffer)
stream << collection;
bytes = buffer.str();
バイト列 -> コレクション
std::string bytes;
RWTValDlist<RWCString> collection;
std::stringbuf buffer(bytes);
RWbistream stream(buffer)
stream >> collection;

上記のようなコードによって、バイト列(std::string)をコレクションの永続化媒体とすることができます。残る作業はバイト列をBLOBに書き込むこと、および読み出されたBLOBからバイト列を抽出することです。

バイト列 <-> BLOB

SourcePro DB(旧DBTools.h++)はBLOBを表現するクラスRWDBBlobを提供しています。RWDBBlobには

バイト列に対するread/writeメソッド:
  • void RDDBBlob::getBytes(void* buffer, size_t size, size_t offset =0) const
  • void RWDBBlob::putBytes(const void* buffer, size_t size, size_t offset -0, size_t resize =256)
およびRWDBBlobが内包するバッファのポインタおよびその大きさを返すメソッド:
  • unsigned char* RWDBBlob::data() const
  • size_t RWDBBlob::length() const

が用意されていますから、これらを用いてBLOBを永続化媒体に用いることができます。

Container -> Blob
template<class Container>
void to_blob(const Container& container, RWDBBlob& blob) {
  std::stringbuf buffer;
  RWbostream strm(&buffer);
  strm << container;
  std::string str = buffer.str();
  blob.clear();
  blob.putBytes(str.data(), str.size());
}
Blob -> Container
template<class Container>
void from_blob(Container& container, RWDBBlob& blob) {
  std::string str(reinterpret_cast<char*>(blob.data()), blob.length());
  std::stringbuf buffer(str);
  RWbistream strm(&buffer);
  container.clear();
  strm >> container;
}

サンプルコード

SourcePro DBを用いて、クラスIntAndStrのコレクションをデータベースに格納するサンプルを以下に示します。

// StdLib
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>

// SourcePro Core
#include <rw/tvdlist.h> // RWTValDlist
#include <rw/bstream.h> // RWbostream, RWbistream

// SourcePro DB
#include <rw/db/dbmgr.h>    // RWDBManager
#include <rw/db/dbase.h>    // RWDBDatabase
#include <rw/db/table.h>    // RWDBTable
#include <rw/db/blob.h>     // RWDBBlob
#include <rw/db/schema.h>   // RWDBSchema
#include <rw/db/inserter.h> // RWDBInserter
#include <rw/db/result.h>   // RWDBResult
#include <rw/db/reader.h>   // RWDBReader

#include "IntAndStr.h"      // IntAndStr

/*
 * definitions related to the database to be connected
 */
#define DB_ACC "msq42"

#if   defined(_RWCONFIG_m)
#define DB_LIB DB_ACC "-m.dll"
#elif defined(_RWCONFIG_md)
#define DB_LIB DB_ACC "-md.dll"
#else
#error sorry...
#endif

#define DB_SERV "MSDE"
#define DB_USER "sa"
#define DB_PASS ""
#define DB_NAME "TRIAL"

/*
 * Containerをcoutにプリントする
 */
template<class Container>
void print(const Container& container) {
  std::copy(container.begin(), container.end(),
            std::ostream_iterator(std::cout, " "));
  std::cout << std::endl;
}

/*
 * Container -> Blob
 */
template<class Container>
void to_blob(const Container& container, RWDBBlob& blob) {
  std::stringbuf buffer;
  RWbostream strm(&buffer);
  strm << container;
  std::string str = buffer.str();
  blob.clear();
  blob.putBytes(str.data(), str.size());
}

/*
 * Blob -> Container
 */
template<class Container>
void from_blob(Container& container, RWDBBlob& blob) {
  std::string str(reinterpret_cast<char*>(blob.data()), blob.length());
  std::stringbuf buffer(str);
  RWbistream strm(&buffer);
  container.clear();
  strm >> container;
}

RWDBDatabase database;

/*
 * テーブルを生成
 */
bool db_init() {
  database = RWDBManager::database(DB_LIB, DB_SERV, DB_USER, DB_PASS, DB_NAME);
  if ( !database.isValid() ) {
    return false;
  }
  std::cerr << database.version() << std::endl;

  RWDBTracer& tracer = database.tracer();
  tracer.stream(std::cout);
  tracer.setOn(RWDBTracer::SQL);
  tracer.setOn(RWDBTracer::BoundBuffers);

  // テーブルがあったら削除
  RWDBTable table = database.table("RWTABLE");
  if ( table.exists(true) ) {
    table.drop();
  }

  // CREATE TABLE ...
  RWDBSchema schema;
  schema.appendColumn("RWKEY",   RWDBValue::String, 10);
  schema.appendColumn("RWVALUE", RWDBValue::Blob, 1024);
  database.createTable(table.name(), schema);

  return true;
}

/*
 * コンテナをBlobとしてテーブルに挿入
 */
bool db_insert() {
  std::cout << "save two collections..." << std::endl;
  // 2つのコレクション en, ja を用意
  RWTValDlist en;
  RWTValDlist ja;

  const size_t N = 10;
  const char* en_str[N] = {
    "zero", "one", "two"l,   "three", "four",
    "five", "six", "seven", "leight", "nine"
  };
  const char* ja_str[N] = {
    "零", "壱", "弐", "参", "四",
    "五", "六", "七", "八", "九"
  };

  for ( int i = 0; i < N; ++i ) {
    en.append(IntAndStr(i, en_str[i]));
    ja.append(IntAndStr(i, ja_str[i]));
  }
  print(en);
  print(ja);

  // en, ja を テーブルに追加
  RWDBTable table = database.table("RWTABLE");
  RWDBInserter inserter = table.inserter();
  RWDBBlob blob;

  to_blob(en, blob);
  inserter << "en" << blob;
  inserter.execute();

  to_blob(ja, blob);
  inserter << "ja" << blob;
  inserter.execute();

  return true;
}

/*
 * テーブルから読み出して復元
 */
bool db_restore() {
  std::cout << "restore two collections..." << std::endl;
  RWDBTable table = database.table("RWTABLE");
  RWDBReader reader = table.reader();
  while ( reader() ) {
    RWCString type;
    RWDBBlob  blob;
    reader >> type >> blob;
    RWTValDlist collection;
    from_blob(collection, blob);
    print(collection);
  }
  return true;
}

void onError(const RWDBStatus& aStatus) {
  if ( aStatus.errorCode() != RWDBStatus::ok ) {
    std::cerr << aStatus.vendorMessage1() << std::endl
              << aStatus.vendorMessage2() << std::endl;
    aStatus.raise();
  }
}

int main() {
  RWDBManager::setErrorHandler(onError);
  int ret = 1;
  try {
    if ( db_init() && db_insert() && db_restore() )
      ret = 0;
  } catch ( RWExternalErr& er) {
    std::cerr << er.why() << std::endl;
  } catch ( exception& ex ) {
    std::cerr << ex.what() << std::endl;
  }
  return ret;
}

おまけ:RWBTreeOnDiskへの永続化

RDBを外部記憶媒体に用いることで大量のデータを管理することができますが、アプリケーションによってはRDBほどに高機能を求められてはおらず、ただ単に検索可能な(主記憶領域には納まらないほどの)大規模辞書を必要とすることがあります。このようなシチュエーションでは、SourePro Coreが提供するディスク上のBTee:RWBTreeOnDiskが使えるかもしれません。

RWBTreeOnDiskは長さ16(この値はコンストラクタの引数で変更できます)以下の文字列またはバイト列をkey、long値をvalueとする(keyの重複を許さない)辞書(key-value対応表)をディスク上に構築/管理します。valueとしてファイルに格納されたデータのシーク位置を設定すれば、文字列をkeyに検索できる簡単なデータベースとして機能します。

RWBTreeOnDiskのデータ・ストレージとして使われているRWFileManager(RWFileの派生クラス)はファイルシステム上であたかもmalloc/freeのような領域確保/解放を管理します。たとえばint data[100] をファイルに書き込むには:

RWFileManager fm("foo.dat");
int data[100];
RWoffset loc = fm.allocate(sizeof(int)*100);
fm.SeekTo(loc);
fm.Write(data,100);

このように、RWFileManagerにファイル上のデータ領域を確保させるにはメソッドallocateの引数に確保する領域の大きさを与えます。

オブジェクトの永続化に必要な領域の大きさを求めるには、前述のバイト列すなわちstd::stringへの書き込み(保存)を行った後、std::stringのメソッドsize()もしくはlength()で得られます。

あるいはRWAuditStreamBufferを使った以下のような実装も可能です。

RWAuditStreamBufferによる領域サイズの取得
RWTValDlist<RWCString> container;
RWAuditStreamBuffer audit;
RWbostream stream(audit);
stream << container;
unsigned long size = audit;

RWAuditStreamBufferに書き込まれたバイト列はどこにも出力されず、バイト列の長さがRWAuditStreamBuffer内部で集計されます。集計された総バイト数はoperator unsigned long()で得られます。

前述のRDBを用いたサンプルと同等の振る舞いを、RWBTreeOnDiskで実装したコードを以下に示します。

// StdLib
#include <iostream>
#include <algorithm>
#include <iterator>

#include <cstdio>
#if defined(_MSC_VER)
namespace std { using ::remove; }
#endif

// SourcePro Core
#include <rw/tvdlist.h>  // RWTValDlist
#include <rw/disktree.h> // RWBTreeOnDisk
#include <rw/filemgr.h>  // RWFileManager
#include <rw/auditbuf.h> // RWAuditStreamBuffer
#include <rw/bstream.h>  // RWbostream/RWbistream
#include <rw/cstring.h>  // RWCString

#include "IntAndStr.h" // IntAndStr

/*
 * Containerをcoutにプリントする
 */
template<class Container>
void print(const Container& container) {
  std::copy(container.begin(), container.end(),
            std::ostream_iterator<Container::value_type>(std::cout, " "));
  std::cout << std::endl;
}

/*
 * Container -> File
 */
template<class Container>
RWoffset to_file(const Container& container, RWFileManager& manager) {
  // containerのsaveに必要なサイズを求める
  RWAuditStreamBuffer audit;
  RWbostream strm(&audit);
  strm << container;
  unsigned long size = audit;
  // 必要な領域をallocateし、saveする
  RWoffset loc = manager.allocate(size);
  manager.SeekTo(loc);
  manager << container;
  return loc;
}

/*
 * File -> Container
 */
template<class Container>
void from_file(Container& container, RWFileManager& manager, long loc) {
  container.clear();
  manager.SeekTo(loc);
  manager >> container;
}

RWBTreeOnDisk* btree;
RWFileManager* manager;

/*
 * コレクションをBTreeに挿入
 */
bool save() {
  std::cout << "save two collections..." << std::endl;
  // 2つのコレクション en, ja を用意
  RWTValDlist<IntAndStr> en;
  RWTValDlist<IntAndStr> ja;

  const size_t N = 10;
  const char* en_str[N] = {
    "zero", "one", "two",   "three", "four",
    "five", "six", "seven", "eight", "nine"
  };
  const char* ja_str[N] = {
    "零", "壱", "弐", "参","四",
    "五", "六", "七", "八", "九"
  };

  for ( int i = 0; i < N; ++i ) {
    en.append(IntAndStr(i, en_str[i]));
    ja.append(IntAndStr(i, ja_str[i]));
  }
  print(en);
  print(ja);

  // numbers, strings を テーブルに追加
  RWoffset loc;
  loc = to_file(en, *manager);
  btree->insertKeyAndValue("en", loc);

  loc = to_file(ja, *manager);
  btree->insertKeyAndValue("ja", loc);

  return true;
}

/*
 * BTreeから読み出して復元
 */
bool restore() {
  std::cout << "restore two collections..." << std::endl;
  const char* keys[] = { "en", "ja" };
  for ( int i = 0; i < 2; ++i ) {
    long loc = btree->findValue(keys[i]);
    if ( loc == RWNIL ) {
      continue;
    }
    RWTValDlist<IntAndStr> collection;
    from_file(collection, *manager, loc);
    print(collection);
  }
  return true;
}

int main() {
  const char* filename = "btree.dat";
  if ( RWFile::Exists(filename) ) {
    std::remove(filename);
  }
  manager = new RWFileManager(filename);
  btree   = new RWBTreeOnDisk(*manager);
  save();
  restore();
  delete btree;
  delete manager;
  return 0;
}