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

はじめてのDBTools.h++ (part-1)

RDBとDBTools.h++

僕は今まで、いわゆるRDB(Rerational DataBase)を使ったアプリケーションを書いたことがありません。好き嫌いはともかく、RDBはソフトウェア世界で最も広く利用されているでしょうし、ソフト屋たるもの、RDBを知らないというわけにもいかないでしょう。

RDBを使ったアプリケーションを、僕の得意とするC++で書くために選んだのがRogue Wave社のDBTools.h++というクラスライブラリです。 DBTools.h++はSybase,Oracle,MS SQL ServerといったRDBエンジンをC++から利用するために作られたクラスライブラリです。

DBTools.h++をインストールし、User's Guideが用意してくれたチュートリアルに取り組んだまではよかったのですが、そこに紹介されているサンプルコードがあまりにキレイすぎて、なんだかよくわからなかったのです。というのもこれらサンプルコードではDBTools.h++によるRDBへのアクセス部がいろんなクラスの中に巧妙に埋め込まれていたため、ソースコードを上から下へ追っかけていけばDBTools.h++の基本的な使い方がわかるというようなシロモノではなかったのです。

そこで僕はUser's Guideが用意してくれたサンプルコードを一旦バラバラに分解し、上から下に追っかければDBTools.h++の大まかな機能と使い方が理解できるようなサンプルを新たに起こすことから始めることにしました。

RDBって何なんだ?

RDBは簡単に言えば複数の「表」に対する様々な操作を行なうソフトウェアです。ここで「表」というのは、まぁぶっちゃけていえば文字列や数をメンバとする構造体の巨大な集合と考えればいいでしょう。たとえばとっても簡単な電話番号簿を作りたい、としましょうか。

struct Person {
  char name[50];  // 名前
  char phone[50]; // 電話番号
};

vector<Person> person;

データベースを使わずに、すべてをon-memoryで処理するつもりなら、上のような構造体(Person)とその集合(person)、およびpersonに対する操作(挿入/削除/変更/検索)を用意することになるでしょう。

personに納めることのできるデータの量はそのアプリケーションに許されるメモリの大きさで決まります。個人が使う電話番号簿ならこれでも十分かもしれません。しかし数百、数千あるいは数万のデータを処理するとなるとディスクの助けを借りないととても歯が立ちません。巨大なデータの塊を処理するのがデータベースなんですね。

プログラマにとってみればディスク上のファイルをシークし、読み出し、書き込むといったディスクアクセスルーチンをちまちまと積み重ねるよりは、データの巨大な集合体があたかもメモリ上に置かれているかのように扱える方が楽に決まっています。

ですからほとんどのRDBは標準となったSQL(System Query Language)でデータベースを操作することで複雑極まりない実際のディスクアクセスを包み隠しています。

しかしながらSQLとオブジェクト指向プログラミングとの間のギャップが少なからず存在するのも事実です。

DBTools.h++はSQLとC++とのギャップをエレガントに埋めてくれるライブラリだと、僕は思っています。

データベースとの接続

ファイル上にある巨大なデータの集合はデータベースソフトウェアの管理下にあり、アプリケーションとは別の空間に存在します。アプリケーションがデータの集合にアクセスするには、まずデータベースに接続しなければなりません。

DBTools.h++でデータベースに接続するには、

RWDBDatabase db = 
  RWDBManager::database("ODBC", "TRIAL", "ALLADIN", "abracadabra", "", "");

RWDBManager::database()には6つの引数を与えます。それぞれサーバタイプ、サーバ名、ユーザ名、パスワード、データベース名、ロールです。この例では、ODBCを使ってデータベース'TRIAL'に接続しています。

DBTools.h++のライブラリは、接続するデータベースに依存しないコア・ライブラリと、データベース毎に異なるアクセス・ライブラリの2つから構成されています。DBTools.h++を使ったアプリケーションを作るときは、コア・ライブラリと、接続するデータベースに応じたアクセス・ライブラリ、そしてTools.h++ライブラリをリンクしてください。

最初の引数であるサーバタイプは接続するデータベースによって異なります。この引数に文字列として何を与えればよいかは、それぞれのアクセス・ライブラリのマニュアルを参照してください。

ちなみに、DBTools.h++がサポートするデータベースすなわちアクセス・ライブラリの種類は以下のとおりです。

  • Oracle
  • Sybase DB
  • Sybase CT
  • MS SQL Server
  • ODBC
  • DB2

データベースを操作するとき、DBTools.h++はその操作に応じたSQLコマンドを生成し、データベースに発行します。

その様子をモニタしたいときはデータベースへの接続が完了した時点で

RWDBTracer& tracer = db.tracer();
tracer.setOn(RWDBTracer::SQL);
tracer.stream(cout);

すれば、発行されたSQLを見ることができます。

また、データベースの操作中にエラーが発生したとき、プロトタイプ

void function(const RWDBStatus&)

である関数を呼び出すことができます。

RWDBStatusのメソッドraise()を呼べば例外を送出してくれますから、

void onError(const RWDBStatus& aStatus) {
   aStatus.raise();
}

int main() {
  RWDBManager::setErrorHandler(onError);
  try {
    // DBTools.h++を使ったデータベース操作
  } catch ( RWExternalErr& er) {
     cout << er.why() << endl;  
     return 1;
  }
  return 0;
}

のようなコードを書いておくと良いでしょう。

テーブルの作成と定義

データベースに接続できたところで、データを格納するテーブルを作成しましょう。デーブル名を'PERSON'とします。

RWDBTable person = db.table("PERSON");

データベース上にテーブル"PERSON"が既に存在していたときは、一旦削除してしまいます。

if ( person.exists(true) ) {
  person.drop();
}

次に、テーブルに登録するレコードがどんな要素から構成されるのか、つまり構造体でいえばメンバの型と名前を定義します。

struct Person {
  char name[50];  // 名前
  char phone[50]; // 電話番号
};

のテーブル上での表現を定義するわけです。

RWDBColumn name(person["NAME"]);
RWDBColumn phone(person["PHONE"]);

テーブル上のひとつのレコードを構成するカラム(項目)としてnameとphoneを用意し、それぞれに名前"NAME","PHONE"を与えました。そしてname,phoneのテーブル上での型やサイズを決めてあげます。

name.type(RWDBValue::String).storageLength(50).nullAllowed(false);
phone.type(RWDBValue::String).storageLength(50).nullAllowed(false);

name,phoneともに文字列、サイズ50、必ず入力しなければならない必須項目とします。

この2つのカラムを持つテーブルを作りましょう。

RWDBSchema schema;
schema.appendColumn(name);
schema.appendColumn(phone);

db.createTable(person.name(), schema);

レコードの追加

それでは試しに、レコードをいくつか追加してみましょう。

struct {
  const char* name;
  const char* phone;
} entries[] = { 
  { "police",    "110" },
  { "fire",      "119" },
  { "weather",   "177" },
  { "time",      "117" },
  { "ambulance", "119" },
  { "episteme",  "045-XXX-XXXX" },
  { "episteme",  "060-XXX-XXXX" },
  { "friend",    "060-XXX-XXXX" },
  { "s34",       "06-XXXX-XXXX" },
  { 0,0 } // end marker
};

RWDBInserter inserter = person.inserter();

for ( int i = 0; entries[i].name; ++i ) {
  inserter << entries[i].name << entries[i].phone;
  inserter.execute();
}

テーブルにレコードを追加するには、テーブルからinserterを取得し、inserterに項目を<<した後にexecute()します。

レコードの読み出し

正しく追加されたかどうか、テーブル内の全レコードを読み出してみます。

RWDBReader   reader;
RWCString    nameVal;
RWCString    phoneVal;

reader = person.reader();
while ( reader() ) {
  reader >> nameVal >> phoneVal;
  cout << setw(20) << nameVal 
       << setw(14) << phoneVal 
       << endl;
}

レコードの読み出しは、テーブルからreaderを取得し、operator()によって次のレコードに移動して>>で項目を取り出します。

実行結果を示します:

              police           110
             weather           177
                time           117
           ambulance           119
            episteme  045-XXX-XXXX
            episteme  060-XXX-XXXX
              friend  060-XXX-XXXX
                 s34  06-XXXX-XXXX
                fire           119

レコードの削除

今度は名前が"friend"であるレコードを削除します。

RWDBDeleter  deleter ;

deleter = person.deleter();
deleter.where(name == "friend");
deleter.execute();

テーブルからdeleterを取得します。

次にwhere()によって削除するレコードが満たす条件を与えます。ここではname=="friend"を与えています。

そしてdeleter.execute()すれば、where()に与えた条件を満たすすべてのレコードが削除されます。

同様に電話番号が"06"から始まるレコードを削除するには:

deleter = person.deleter();
deleter.where(phone.like("06%"));
deleter.execute();

メソッドlikeに与えた文字列中にある'%'はワイルドカード、すなわち任意の文字列を表します。

              police           110
             weather           177
                time           117
           ambulance           119
            episteme  045-XXX-XXXX
                fire           119

レコードの更新

名前が"weather"であるレコードを"forecast"に変更してみましょう。

RWDBUpdater updater;

updater = person.updater();
updater << name.assign("forecast");
updater.where(name == "weather");
updater.execute();

まずテーブルからupdaterを取得します。

次に << で変更するカラムと値を設定します。

ここで変更を要求しなかったカラムは既存の値のままとなります。そしてwhere()で更新の対象となるレコードの条件を与え、execute()します。

              police           110
            forecast           177
                time           117
           ambulance           119
            episteme  045-XXX-XXXX
                fire           119

レコードの検索

RDBでは特定の条件を満たすレコードを検索する処理が最も頻繁に行なわれることでしょう。

レコードの件策にはselectorが用いられます。たとえば電話番号が"11"から始まるレコードを見つけるには、

RWDBSelector selector;

selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
selector.execute();
reader = selector.reder()

データベースからselectorを取得します。テーブルからではないことに注意してください。なぜなら、複数のテーブルに対して検索条件を指定することがあるからです。

得られたselectorに対し <<
によって検索条件に適合するレコードのカラム群を与えます。ここではテーブルを丸ごと与えることでテーブルないの全カラムを取り出すことにします。

次に、where()によって検索条件を指定し、execute()で検索を実行します。

検索が完了したらserectorからreaderを取得します。あとはreaderから検索結果を読み出します。

              police           110
                time           117
           ambulance           119
                fire           119

where()には、各条件を && や || で繋ぐことで、複雑な検索条件で検索できます。

電話番号が"119"、または名前に's'が含まれるレコードを検索するには、

selector = db.selector();
selector << person;
selector.where(phone == "119" || name.like("%s%"));
selector.execute();
            forecast           177
           ambulance           119
            episteme  045-XXX-XXXX
                fire           119

selectorには検索条件だけでなく、結果のソート条件も設定できます。

selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
selector.orderBy(phone);
selector.orderBy(name);
selector.execute();
reader = selector.reder()

この例では、検索結果を電話番号、名前の順にソートしています。

              police           110
                time           117
           ambulance           119
                fire           119

              

検索結果の一括取得

RWTPtrMemTableを使えば、selectorによる検索結果をメモリ上のコンテナに一気に読み込むことができます。

まず、レコードの各カラムを格納するクラスRecordを作り、RWDBReaderからの読み込みを行なうoperator>>()を定義しておきます。


struct Record {
  RWCString name;
  RWCString phone;
};

RWDBReader& operator>>(RWDBReader& reader, Record& record) {
  return reader >> record.name >> record.phone;
}  

// これはオマケ。
ostream& operator<<(ostream& strm, const Record& record) {
  strm << setw(20) << record.name 
       << setw(14) << record.phone 
       << endl;
  return strm;
}

検索自体は前述のとおり、

selector = db.selector();
selector << person;
selector.where(phone.like("11%"));

ここまでは同じなのですが、execute()せずに、

RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > founds(selector);

とやると、検索の結果がfoundsに一括して登録されます。

RWDBTPtrMemTableの第2
template引数にはレコードを格納するコンテナを与えます。Tools.h++の提供するコンテナRWTPtrOrderedVector, RWTPtrSlist, RWTPtrDiistなどが指定できます。

読み出してみましょう。

for ( i = 0; i < founds.entries(); ++i ) {
  cout << *founds.at(i);
}
founds.clearAndDestroy();

最後のclearAndDestroy()は、operator new()によって取得されたRecordをシステムに返却するためのものです。


              police           110
                time           117
           ambulance           119
                fire           119

また、RWDBTPtrMemTableのコンストラクタにはselectorだけでなく、テーブル(RWDBTable)やreader(RWDBReader)も与えることができますから、テーブル全体の一括読み出しも可能です。

typedef RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > RecVector;
RecVector records(person);
for ( i = 0; i < records.entries(); ++i ) {
  cout << *records.at(i);
}
records.clearAndDestroy();

              police           110
            forecast           177
                time           117
           ambulance           119
            episteme  045-XXX-XXXX
                fire           119

いかがでしょうか、SQLの心得のある方なら、DBTools.h++を使ったデータベースの操作がC++のオブジェクトにとても上手にマッピングされているのが理解できるかと思います。

ここまでの各操作をまとめてテストするコードを以下に示します。

#include <windows.h> 
#include <iostream>
#include <iomanip>
#include <rw/db/dbmgr.h>
#include <rw/db/db.h>
#include <rw/db/tpmemtab.h>
#include <rw/tpordvec.h>

using namespace std;

/* 発行されているSQLコマンドをモニタしたいときは
 *  以下のコメントを外してください
 */
// #define TRACE

struct Record {
  RWCString name;
  RWCString phone;
};

RWDBReader& operator>>(RWDBReader& reader, Record& record) {
  return reader >> record.name >> record.phone;
}  

ostream& operator<<(ostream& strm, const Record& record) {
  strm << setw(20) << record.name 
       << setw(14) << record.phone 
       << endl;
  return strm;
}

void dump(RWDBReader& reader) {
  int count = 0;
  Record record;
  while ( reader() ) {
    reader >> record;
    cout << record;
    ++count;
  }
  cout << count << " records." << endl << endl;
}

int trial() {

  // データベースに接続
  cout << "データベース'TRIAL'に接続します。" << endl;
  RWDBDatabase db = RWDBManager::database(
                       "ODBC",        // serverType
                       "TRIAL",       // serverName
                       "ALLADIN",     // userName
                       "abracadabra", // password
                       "",            // databaseName
                       ""             // role
                       );

  if ( !db.isValid() ) return 1;

#ifdef TRACE
  RWDBTracer& tracer = db.tracer();
  tracer.setOn(RWDBTracer::SQL);
  tracer.stream(cout);
#endif

  // テーブルを作成
  RWDBTable person = db.table("PERSON");
  if ( person.exists(true) ) {
    cout << "テーブル'PERSON'を削除します。" << endl;
    person.drop();
  }

  cout << "テーブル'PERSON'を作成します" << endl;

  RWDBColumn name(person["NAME"]);
  RWDBColumn phone(person["PHONE"]);

  name .type(RWDBValue::String)
       .storageLength(50)
       .nullAllowed(false);
  phone.type(RWDBValue::String)
       .storageLength(50)
       .nullAllowed(false);

  RWDBSchema schema;
  schema.appendColumn(name);
  schema.appendColumn(phone);

  db.createTable(person.name(), schema);

  // レコードを追加
  cout << "レコードを追加します。" << endl;
  struct {
    const char* name;
    const char* phone;
  } entries[] = { 
    { "police",    "110" },
    { "fire",      "119" },
    { "weather",   "177" },
    { "time",      "117" },
    { "ambulance", "119" },
    { "episteme",  "045-XXX-XXXX" },
    { "episteme",  "060-XXX-XXXX" },
    { "friend",    "060-XXX-XXXX" },
    { "s34",       "06-XXXX-XXXX" },
    { 0,0 } // end marker
  };

  RWDBInserter inserter = person.inserter();

  for ( int i = 0; entries[i].name; ++i ) {
    inserter << entries[i].name << entries[i].phone;
    inserter.execute();
  }
  Sleep(1000);

  RWDBReader   reader;
  RWDBSelector selector;
  RWDBDeleter  deleter ;
  RWDBUpdater  updater;

  // 全レコードの読み出し 
  cout << "追加されたレコードは:" << endl;
  dump(person.reader());

  // レコードの削除
  cout << "電話番号が'06'で始まるレコードを削除します" << endl;
  deleter = person.deleter();
  deleter.where(phone.like("06%"));
  deleter.execute();
  Sleep(1000);

  dump(person.reader());

  // レコードの更新
  cout << "'weather'を'forecast'に変更します" << endl;
  updater = person.updater();
  updater << name.assign("forecast");
  updater.where(name == "weather");
  updater.execute();
  Sleep(1000);

  dump(person.reader());

  // 条件によるレコードの読み出し 
  cout << "電話番号が'11'から始まるレコードは:" << endl;
  selector = db.selector();
  selector << person;
  selector.where(phone.like("11%"));
  selector.execute();

  dump(selector.reader());

  // 条件によるレコードの読み出し-2
  cout << "電話番号が'11'から始まるレコードは:" << endl;
  selector = db.selector();
  selector << person;
  selector.where(phone.like("11%"));

  typedef RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > RecVector;
  RecVector founds(selector);
  for ( i = 0; i < founds.entries(); ++i )
    cout << *founds.at(i);
  cout << endl;
  founds.clearAndDestroy();

  // 全レコードの読み出し 
  cout << "現在の全レコードは:" << endl;
  RecVector records(person);
  for ( i = 0; i < records.entries(); ++i )
    cout << *records.at(i);
  cout << endl;
  records.clearAndDestroy();

  return 0;
}

void onError(const RWDBStatus& aStatus) {
   aStatus.raise();
}

int main() {
  RWDBManager::setErrorHandler(onError);
  int ret = 0;
  try {
    ret = trial();
  } catch ( RWExternalErr& er) {
     cout << er.why() << endl; 
  }
  return ret;
}