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

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

結合された複数テーブルからの読み出し

[part-1]に引き続き、DBTools.h++を使ったRDBの操作法に関する解説を続けます。

[part-1]ではひとつのテーブルに対するレコードの追加/変更/削除/検索を行ないました。では、結合された複数のテーブルから検索条件を指定して読み出してみる事にしましょう。

デモンストレーションのために、次のようなデータを用意しました。

NAME KIND HEADER
accumulate function numeric
adjacext_difference function numeric
adjacext_find function algorithm

これはStandard Template Libraryが提供する関数/構造体/クラスの

  • NAME : 名前
  • KIND : 種別 (関数/構造体/クラス)
  • HEADER : 宣言されているヘッダ

を一覧にした表です。まず各フィールドを以下のような3つのテーブルに分割しました。

T_STL
フィールド名 サイズ
NAME CHAR 32
KIND_ID LONG 4
HEAD_ID LONG 4
T_KIND
フィールド名 サイズ
ID LONG 4
NAME CHAR 8
T_HEAD
フィールド名 サイズ
ID LONG 4
NAME CHAR 16

T_STLには関数/構造体/クラスの名前と、種別/ヘッダのIDが格納されます。種別/ヘッダ名はそれぞれT_KIND/T_HEADのIDに対応するNAMEフィールドに格納します。

これらのテーブルから、元の形式のデータを手に入れるには、

T_STL.KIND_ID と T_KIND.ID が等しく

かつ

T_STL.HEAD_ID と T_HEAD.ID が等しい

であるようなレコードの組み合わせから、

T_STL.NAME と T_KIND.NAME と T_HEAD.NAME を抽出すればいいわけです。

SQLで表現すると:

SELECT T_STL,NAME, T_KIND.NAME, T_HEAD.NAME
FROM   T_STL, T_KIND, T_HEAD
WHERE  T_STL.KIND_ID = T_KIND.ID AND T_STL.HEAD_ID = T_HEAD.ID

となります。

これをDBTools.h++で実現してみましょう。DBTools.h++による複数テーブルからの検索は、単一テーブルからの検索と何ら変わるところがありません。

まずデータベースをオープンし、テーブルを用意します。

  RWDBDatabase db_;
  RWDBTable    stl_;
  RWDBTable    kind_;
  RWDBTable    head_;

  db_ = RWDBManager::database(
     // serverType, serverName,  userName, password, databaseName
        SERVER,     "MASTER",    "FRUIT",  "APPLE",  "LIBRARIES"
        );

  stl_  = db_.table("T_STL");
  kind_ = db_.table("T_KIND");
  head_ = db_.table("T_HEAD");

つぎにデータベースからselectorを手に入れ、抽出フィールドと検索条件を設定します。

  RWDBSelector selector_;

  selector_ = db_.selector();
  selector_ << stl_["NAME"] << kind_["NAME"] << head_["NAME"];
  selector_.where(stl_["KIND_ID"] == kind_["ID"] &&
                  stl_["HEAD_ID"] == head_["ID"])

あとはselectorから得られたreaderを使ってフィールドを取り出します。

  RWDBReader reader_;
  reader_ = selector_.reader();
  while ( reader_() ) {
    RWCString name, kind, header;
    reader_ >> name >> kind >> header;
    ....
  }

自己結合

上の例は、複数のテーブル上の特定のフィールドを他のテーブルのフィールドと結合させています(内部結合:Inner Join)。

次の例では、あるフィールドを"同じテーブル"のフィールドと結合させます(自己結合:Self Join)。

T_FAMILY
フィールド名 サイズ
ID LONG 4
NAME CHAR 8
FATHER_ID LONG 4

このテーブルには、ある人のIDと名前、そしてその人の父親のIDが定義されています。そして父親のレコードもまた、同じテーブル上に格納するものとします。

このとき、親子関係にある二人の名前の組を検索してみましょう。

SQLでは以下のようになります:

SELECT first.NAME, second.NAME
FROM   T_FAMILY first, T_FAMILY second
WHERE  first.ID = second.FATHER_ID

DBTools.h++では、ひとつのテーブルから複数のRWDBTableを作ることで実現します:

RWDBTable first  = db.table("T_FAMILY");
RWDBTable second = db.table("T_FAMILY");
RWDBSelector select = db.selector();
select << first["NAME"] << second["NAME"];
select.where(first["ID"] == second["FATHER_ID"]);

カーソルによるナビゲーション

前の例ではselectorからreaderを取り出していましたが、これでは最初のレコードから順に取り出す事しかできません。

カーソルを使えば、上のようにして得られたレコードの集合に対し、

  • 最初のレコード
  • 最後のレコード
  • 次のレコード
  • 前のレコード

を取り出すことができます。

まず、selectorからcursorを手に入れます。

  RWDBCursor cursor_;
  cursor_ = selector_.cursor(RWDBCursor::Scrolling);

selectorのメソッドcursorの引数としてRWDBCursor::Scrollingを与えています。これを省略するとRWDBCursor::Sequentialとみなされ、順次読み出ししかできません。

次にカーソルから読み出されるデータを変数にアタッチします。

  RWCString name, kind, header;
  cursor << &name << &kind << &header;

これによって、カーソルが移動すると同時にカーソルがポイントするレコードの各フィールドの内容が変数にセットされます。

カーソルの移動はfetchRow()メソッドで行ないます。

  • cursor_.fetchRow(RWDBCursor::First); //
    先頭へ
  • cursor_.fetchRow(RWDBCursor::Last); //
    末尾へ
  • cursor_.fetchRow(RWDBCursor::Next); //
    次へ
  • cursor_.fetchRow(RWDBCursor::Previous);
    // 前へ

下図のようなアプリケーションを作ってみました。

アプリケーション

ダイアログの中ほどにある4つのボタンで先頭/前/次/末尾に移動します。

ソースコードは以下のようになります。

// ------------- definition ---
#ifndef _DBTOOLSDLG_H_
#define _DBTOOLSDLG_H_

class DBToolsDlg : public CDialog {
private:
    RWDBDatabase db_;
    RWDBTable    stl_;
    RWDBTable    kind_;
    RWDBTable    head_;
    RWDBSelector selector_;
    RWDBCursor   cursor_;

    static CDialog* self_;

private:
    static void onError(const RWDBStatus& status);
    void initDB();  // データベースとの接続
    void display(); // 検索結果の表示

public:
    DBToolsDlg(CWnd* pParent = NULL);

    //{{AFX_DATA(DBToolsDlg)
    enum { IDD = IDD_DBTOOLS_4_DIALOG };
    //}}AFX_DATA

    //{{AFX_VIRTUAL(DBToolsDlg)
    //}}AFX_VIRTUAL

protected:
    HICON m_hIcon;

    //{{AFX_MSG(DBToolsDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    afx_msg void OnFirst();  // 先頭レコードへ
    afx_msg void OnLast();   // 末尾レコードへ
    afx_msg void OnNext();   // 次のレコードへ
    afx_msg void OnPrev();   // 前のレコードへ
    afx_msg void OnSearch(); // 検索開始
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

#endif

// ------------- implementation ---
#include "stdafx.h"
#include "DBTools_4.h"
#include "DBToolsDlg.h"

DBToolsDlg::DBToolsDlg(CWnd* pParent /*=NULL*/)
  : CDialog(DBToolsDlg::IDD, pParent) {
    //{{AFX_DATA_INIT(DBToolsDlg)
    //}}AFX_DATA_INIT
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
    self_ = this;
}

BEGIN_MESSAGE_MAP(DBToolsDlg, CDialog)
    //{{AFX_MSG_MAP(DBToolsDlg)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_FIRST, OnFirst)
    ON_BN_CLICKED(IDC_LAST, OnLast)
    ON_BN_CLICKED(IDC_NEXT, OnNext)
    ON_BN_CLICKED(IDC_PREV, OnPrev)
    ON_BN_CLICKED(IDC_SEARCH, OnSearch)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

void DBToolsDlg::OnSysCommand(UINT nID, LPARAM lParam) {
  if ((nID & 0xFFF0) == IDM_ABOUTBOX) {
    CAboutDlg dlgAbout;
    dlgAbout.DoModal();
  } else {
    CDialog::OnSysCommand(nID, lParam);
  }
}

void DBToolsDlg::OnPaint() {
  if (IsIconic()) {
    CPaintDC dc(this); // 描画用のデバイス コンテキスト
    SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
    CRect rect;
    GetClientRect(&rect);
    int x = (rect.Width() - GetSystemMetrics(SM_CXICON) + 1) / 2;
    int y = (rect.Height() - GetSystemMetrics(SM_CYICON) + 1) / 2;
    dc.DrawIcon(x, y, m_hIcon);
  } else {
    CDialog::OnPaint();
  }
}

HCURSOR DBToolsDlg::OnQueryDragIcon() {
    return (HCURSOR) m_hIcon;
}

/*
 * サーバ・タイプ: Debug mode   - msq15d.dll
 *                 Release mode - msq12d.dll
 */
#ifdef RWDEBUG
#define SERVER "msq15d.dll"
#else
#define SERVER "msq12d.dll"
#endif

CDialog* DBToolsDlg::self_ = 0;

namespace {

  void none(const RWDBStatus&) {
    // nothing to do...
  }

  inline void set_bitmap(CWnd* wnd, UINT n) {
    HBITMAP bmp = LoadBitmap(AfxGetInstanceHandle(),MAKEINTRESOURCE(n));
    static_cast<CButton*>(wnd)->SetBitmap(bmp);
  }

  struct record {
    RWCString name;
    RWCString kind;
    RWCString head;
    void clear() { name = ""; kind = ""; head = ""; }
  } rec;
}

BOOL DBToolsDlg::OnInitDialog() {
  CDialog::OnInitDialog();

  ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
  ASSERT(IDM_ABOUTBOX < 0xF000);
  CMenu* pSysMenu = GetSystemMenu(FALSE);
  if (pSysMenu != NULL) {
    CString strAboutMenu;
    strAboutMenu.LoadString(IDS_ABOUTBOX);
    if (!strAboutMenu.IsEmpty()) {
      pSysMenu->AppendMenu(MF_SEPARATOR);
      pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
    }
  }

  SetIcon(m_hIcon, TRUE);
  SetIcon(m_hIcon, FALSE);

  set_bitmap(GetDlgItem(IDC_FIRST),IDB_FIRST);
  set_bitmap(GetDlgItem(IDC_PREV), IDB_PREV);
  set_bitmap(GetDlgItem(IDC_NEXT), IDB_NEXT);
  set_bitmap(GetDlgItem(IDC_LAST), IDB_LAST);

  initDB();
  SetDlgItemText(IDC_PATTERN, "%");
  static_cast<CButton*>(GetDlgItem(IDC_RNAME))->SetCheck(1);
  OnSearch();
  return TRUE;
}

// データベースとの接続とテーブルの取得
void DBToolsDlg::initDB() {
  RWDBManager::setErrorHandler(onError);

  db_ = RWDBManager::database(
     // serverType, serverName,   userName, password, databaseName
        SERVER,     "GATEWAY_FF", "sa",     "",       "TRIAL"
        );

  stl_  = db_.table("T_STL");
  kind_ = db_.table("T_KIND");
  head_ = db_.table("T_HEAD");
}

// 先頭レコードへ
void DBToolsDlg::OnFirst() {
  cursor_.fetchRow(RWDBCursor::First);
  display();
}

// 末尾レコードへ
void DBToolsDlg::OnLast() {
  cursor_.fetchRow(RWDBCursor::Last);
  display();
}

// 次のレコードへ
void DBToolsDlg::OnNext() {
  if ( cursor_.fetchRow(RWDBCursor::Next).isValid() )
    display();
  else {
    MessageBeep(0);
    OnLast();
  }
}

// 前のレコードへ
void DBToolsDlg::OnPrev() {
  if ( cursor_.fetchRow(RWDBCursor::Previous).isValid() )
    display();
  else {
    MessageBeep(0);
    OnFirst();
  }
}

// レコードの表示
void DBToolsDlg::display() {
  SetDlgItemText(IDC_NAME, rec.name.data());
  SetDlgItemText(IDC_KIND, rec.kind.data());
  SetDlgItemText(IDC_HEAD, rec.head.data());
}

// エラーハンドラ : エラーメッセージを出力
void DBToolsDlg::onError(const RWDBStatus& aStatus) {
  if ( aStatus.errorCode() != RWDBStatus::ok ) {
    AfxMessageBox(aStatus.message().data());
    self_->EndDialog(IDCANCEL);
  }
}

// 検索
void DBToolsDlg::OnSearch() {

  RWDBColumn column;
  switch ( GetCheckedRadioButton(IDC_RNAME,IDC_RHEAD) ) {
  case IDC_RHEAD :
    column = head_["NAME"]; break;
  case IDC_RKIND :
    column = kind_["NAME"]; break;
  case IDC_RNAME : // no break, fall through...
  default:
    column = stl_["NAME"]; break;
  }

  /* 生成されるSQL文は:
   *
   * SELECT
   *   T_STL,NAME,
   *   T_KIND.NAME,
   *   T_HEAD.NAME
   * FROM
   *   T_STL,
   *   T_KIND,
   *   T_HEAD
   * WHERE
   *   T_STL.KIND_ID = T_KIND.ID
   *     AND
   *   T_STL.HEAD_ID = T_HEAD.ID
   *     AND
   *   COLUMN LIKE 'STRING'
   * ORDER BY
   *   COLUMN ASC
   *
   * と等価である。
   * なお 上記SQLにおいて COLUMN, 'STRING' はユーザが設定する
   */

  CString key;
  GetDlgItemText(IDC_PATTERN, key);
  RWDBCriterion join = stl_["KIND_ID"] == kind_["ID"] &&
                       stl_["HEAD_ID"] == head_["ID"];
  selector_ = db_.selector();
  selector_ << stl_["NAME"] << kind_["NAME"] << head_["NAME"];
  selector_.where(join && column.like(static_cast<const char*>(key)));
  selector_.orderBy(column);

  // 生成されたSQL文を表示
  SetDlgItemText(IDC_SQL, selector_.asString());
  /*
   * 次のようなSQL文が生成される:
   *
   * SELECT
   *   t3.NAME,
   *   t4.NAME,
   *   t5.NAME
   * FROM
   *   T_STL t3,
   *   T_KIND t4,
   *   T_HEAD t5
   * WHERE
   *   t3.KIND_ID = t4.ID
   *     AND
   *   t3.HEAD_ID = t5.ID
   *     AND
   *   t3.NAME LIKE '%'
   * ORDER BY
   *   t3.NAME ASC
   */

  // カーソルを取得
  cursor_ = selector_.cursor(RWDBCursor::Scrolling);
  cursor_ << &rec.name << &rec.kind << &rec.head;
  cursor_.setErrorHandler(none);
  rec.clear();
  // 先頭レコードへ
  OnFirst();
}

トランザクション

複数のテーブルでひとつのデータの組を表現しているとき、データの更新(追加/削除/変更)を行なうには、当然ながら複数のテーブルに対して追加/削除/変更しなければなりません。

このとき、いずれかのテーブルの更新に失敗したとき、他のテーブルに対して行なった更新をキャンセルしてあげないと、データの整合が狂ってしまいます。すなわち、対象となる全テーブルの更新が成功しなかったときは、更新前の状態に戻してあげなければなりません。

DBTools.h++ではconnectionを使って変更の確定とキャンセルを行ないます。

データベースからconnectionを手に入れます。

  RWDBConnection con;
  con = db_.connection();
  con.autoCommit(false);

テーブルの更新に先立って、トランザクションを開始します。

  con.beginTransaction();

以後、テーブルに対する操作(追加/削除/変更)は、connectionを引数に渡します。

たとえばレコードの追加であれば:

  RWDBInserter inserter = stl_.inserter();
  //          NAME , KIND_ID, HEAD_ID
  inserter << "some_function" << 10 << 4;
  inserter.execute(con);

もし一連のテーブルの更新の途中でエラーが発生したとき、

  con.rollbackTransaction();

すれば、全テーブルの内容は con.beginTransaction() を行なった時点の状態まで巻き戻されます。

全テーブルの更新が成功したときは

  con.commitTransaction();

することで、全テーブルの内容が"確定"します。

エラーハンドラと例外を使えば、一連のトランザクション処理を以下のように書く事ができます:

void onError(const RWDBStatus& aStatus) {
  // エラーが発生したら例外をthrowする
  if ( aStatus.errorCode() != RWDBStatus::ok )
    aStatus.raise();
}

...
// エラーハンドラを設定する
RWDBManager::setErrorHandler(onError);

RWDBConnection con = db_.connection();
con.autoCommit(false);
try {
  con.beginTransaction();
  // テーブル更新処理
  // ...
  con.commitTransaction();
} // エラーが発生したらロールバック(巻き戻し)する
catch ( RWExternalErr&) {
  con.rollbackTransaction();
}

一括読み出し/書き込み

テーブルに対するレコードの読み出し/追加にはRWDBReader/REDBInserterを使います。が、RWDBReader/RWDBInserterは読み出し/書き込みごとにRDBへのアクセスを行ないます。したがって、大量のレコードを読み出す/書き込むときにはRDBへのアクセス回数が非常に多いため、パフォーマンスに問題があります。RWDBBulkReader/RWBulkInserterを使えば、一回のRDBアクセスで複数のレコードを一気に読み込む/書き込むことができます。

RWDBConnection con = db_.connection();

// 10レコードづつ一括読み込み
RWDBBulkReader bulk_r = stl_.bulkReader(con);
RWDBStringVector names(10, 32); // 32文字の文字列を10個分
RWDBVector<long> kinds(10);
RWDBVector<long> heads(10);
bulk_r << names << kinds << heads;

int numsRead = 0;
while ( (numsRead = bulk_r()) != 0 ) {
  for ( int i = 0; i << numsRead; ++i ) {
    cout << names[i] << ' '
         << kinds[i] << ' '
         << heads[i]
         << endl;
  }
}

// 10レコード一括書き込み
RWDBBulkInserter bulk_i = stl_.bulkInserter(con);
bulk_i << names << kinds << heads;
for ( int i = 0; i < 10; ) {
  names[i] = ...
  kinds[i] = ...
  heads[i] = ...
}
bulk_i.execute();

プライマリ・キー(PrimaryKey)と外部キー(Foreign Key)

テーブルの作成時、フィールドにプライマリ・キー(Primary Key)を設定することにより、データベースのパフォーマンスを向上させることができます。

また、そのプライマリ・キーを別のテーブルのフィールドの外部キーに設定しておけば、テーブル間の整合を保つことができます。

  // T_KIND のスキーマ定義
  RWDBSchema  kindSchema;

  RWDBColumn kind_id;
  kind_id.name("ID").type(RWDBValue::Long).nullAllowed(FALSE);
  kindSchema.appendColumn(kind_id);

  RWDBColumn kind_name;
  kind_name.name("NAME").type(RWDBValue::String).nullAllowed(FALSE).storageLength(8);
  kindSchema.appendColumn(kind_name);

  // ID を T_KIND のプライマリ・キーに設定する
  RWDBSchema  primeKey;
  primeKey.appendColumn(kindSchema["ID"]);
  kindSchema.primaryKey(primeKey);

  // テーブル T_KIND を作成
  db_.createTable("T_KIND", kindSchema);

  // T_STL のスキーマ定義
  RWDBSchema stlSchema;

  RWDBColumn stl_name;
  stl_name.name("NAME").type(RWDBValue::String).nullAllowed(FALSE).storageLength(50);
  stlSchema.appendColumn(stl_name);

  RWDBColumn stl_kind_id;
  stl_kind_id.name("KIND_ID").type(RWDBValue::Long).nullAllowed(FALSE);
  stlSchema.appendColumn(stl_kind_id);

  RWDBColumn stl_head_id;
  stl_head_id.name("HEAD_ID").type(RWDBValue::Long).nullAllowed(FALSE);
  stlSchema.appendColumn(stl_head_id);

  // T_KIND.ID を T_STL.KIND_ID の外部キーに設定する
  RWDBForeignKey keyToKind("KIND_ID");
  keyToKind.appendColumn(kindSchema["ID"]);
  stlSchema.foreignKey(keyToKind);

  // テーブル T_STL を作成
  db_.createTable("T_STL", stlSchema);

マルチスレッド環境下での排他制御

複数のクライアントからの要求に応じてデータベースの操作を行なうアプリケーションを想定してみてください。クライアントからのいくつもの要求に迅速に対応するには、マルチスレッド化が不可欠となります。しかし、その場合同一のテーブルに対するレコードの追加/削除/変更および検索要求がいくつも飛び込んできます。このとき、それぞれの要求に対してきちんと排他制御を行なわないとテーブルの不整合が発生したり、誤った検索結果を返すことになるでしょう。

DBTools.h++はスレッド間の競合を防ぐため、基本的な排他制御メカニズムを提供しています。

DBTools.h++の提供する多くのクラスには、メソッドacquire()とrelease()が定義されています。

acquire()によってそのオブジェクトの使用権を取得します。このとき、他のスレッドによって使用中であった場合、そのオブジェクトが使用可能となるまでブロックされます(acquire()から戻ってこない)。

release()によってそのオブジェクトの使用権を放棄します。acquire()でブロックしているスレッドがあれば、そのスレッドが動き始めます。

このメカニズムを利用すれば、ある瞬間においてオブジェクトを使用しているスレッドをひとつだけに制限する、すなわち排他制御が可能となります。

RWDBConnection sharedConn;

void foo(const RWDBDatabase& database) {
  sharedConn.acquire();
  {
    RWDBStoredProc sp = database::storedProc("myProc", sharedConn);
    sp.execute(sharedConn);
  }
  sharedConn.release();
  return;
}

上記の例は、ストアド・プロシージャを実行するものです。ストアド・プロシージャの実行に先立ちsharedConnに対してacquire()することで、sharedConnの使用権を取得しています。その後saredConn.release()されるまでは、他のスレッドがsharedConnを使うことができません。こうすることで、ストアド・プロシージャに対し、それを実行するスレッドはただ一つであることを保証します。

ただし、このコードには小さな穴があいています。acuire()からrelease()までの間に例外が発生した場合です。例外が発生すると、この関数のreturnに到達することなく、catchブロックに飛び込みます。したがってこの関数のreturnの直前にあるrelease()が呼ばれることはなく、結果的にsharedConnは使用権が解放されないままになってしまい、その後いかなるスレッドもこの関数をコールしたとたんブロックされることでしょう。

このようなデッドロックを避けるには、この関数の中で例外を捕まえ、そこでrelease()することになるでしょう。

RWDBConnection sharedConn;

void foo(const RWDBDatabase& database) {
  sharedConn.acquire();
  try {
    RWDBStoredProc sp = database::storedProc("myProc", sharedConn);
    sp.execute(sharedConn);
  } catch ( RWExternalErr&) {
    sharedConn.release();
    throw; // 再throw
  }
  sharedConn.release();
  return;
}

Guardクラスを用意し、そのコンストラクタでacquire(),デストラクタでrelease()すると便利です。

RWDBConnection sharedConn;

template<class T>
class Guard {
  T& object_;
public:
  Guard(const T& t) : object_(t) { object_.acquire(); }
 ~Guard() { object_.release(); }
};

void foo(const RWDBDatabase& database) {
  Guard<RWDBConnection> guard(sharedConn);
  {
    RWDBStoredProc sp = database::storedProc("myProc", sharedConn);
    sp.execute(sharedConn);
  }
  return;
}

こうしておけば、例外が発生したときGuardのデストラクタがrelease()してくれるのでデッドロックを回避できます。

SQLの直接実行

このように、DBTools.h++はSQLをまったく記述せずにRDBの様々な操作を可能にしてくれます。

しかしながら、特定のRDBだけがサポートするサービスの利用など、場合によってはどうしてもSQLでないと実行できないものもあります。そんなときのために、DBTools.h++はSQLの直接実行を許しています(マニュアルには"お勧めしない"と明記されていますが…)。

以下はRogue Waveのサポートチームが提供してくれた、SQL直接実行のサンプルです。コメントは適宜和訳しました。

/*
** 以下のSQL-SELECT文を実行するプログラム
** (外部結合の入れ子はDBTools.h++では未サポート)
**
**  SELECT t0.NAME, t1.NAME
**  FROM T_JOB t1 RIGHT JOIN
**   (T_EMP t0 INNER JOIN T_MAP t2
**      on t0.ID = t2.EMP_ID)
**   on t1.ID = t2.JOB_ID
*/

#include <rw/rstream.h>
#include <rw/db/db.h>

void errorHandler (const RWDBStatus& aStatus) {
  // エラーメッセージを出力
  if (((int)aStatus.errorCode()) != 0) {
    cout << "Error code:       " << (int) aStatus.errorCode() << endl
           << "Error message     " << aStatus.message() << endl
           << "Is terminal:      " << (aStatus.isTerminal() ? "Yes" : "No") << endl
           << "Vendor error 1:   " << aStatus.vendorError1() << endl
           << "Vendor error 2:   " << aStatus.vendorError2() << endl
           << "Vendor message 1: " << aStatus.vendorMessage1() << endl
           << "Vendor message 2: " << aStatus.vendorMessage2() << endl;
  }
}

int main()  {

  // エラーハンドラをセット
  RWDBManager::setErrorHandler(errorHandler);

  // データベースとの接続
  // (接続パラメータは環境に合わせて適宜変更すること)
  RWDBDatabase dataBase =
    RWDBManager::database("library","server","user","password","");

  // コネクションの生成
  RWDBConnection conn = dataBase.connection();

  // SQL文のトレース
  RWDBTracer& tracer = dataBase.tracer();
  tracer.setOn(RWDBTracer::SQL);
  tracer.stream(cout);

  // 3つのテーブル 'T_EMP', 'T_JOB', 'T_EMP' を作成
  RWDBTable tab1 = dataBase.table("T_EMP");
  RWDBTable tab2 = dataBase.table("T_JOB");
  RWDBTable tab3 = dataBase.table("T_MAP");
  if(!tab1.exists(conn, TRUE)) {
    conn.executeSql("create table T_EMP(ID int, NAME char(10))");
    conn.executeSql("insert into T_EMP values(1, 'bill')");
    conn.executeSql("insert into T_EMP values(2, 'sally')");
    conn.executeSql("insert into T_EMP values(3, 'dave')");
    conn.executeSql("insert into T_EMP values(4, 'ted')");
    conn.executeSql("insert into T_EMP values(5, 'george')");
  }

  if(!tab2.exists(conn, TRUE)) {
    conn.executeSql("create table T_JOB(ID int, NAME char(10))");
    conn.executeSql("insert into T_JOB values(1, 'porter')");
    conn.executeSql("insert into T_JOB values(2, 'smelter')");
    conn.executeSql("insert into T_JOB values(3, 'fisher')");
    conn.executeSql("insert into T_JOB values(4, 'bell hop')");
    conn.executeSql("insert into T_JOB values(5, 'cook')");
  }

  if(!tab3.exists(conn, TRUE)) {
    conn.executeSql("create table T_MAP(EMP_ID int, JOB_ID int)");
    conn.executeSql("insert into T_MAP values(1, 1)");
    conn.executeSql("insert into T_MAP values(2, 2)");
    conn.executeSql("insert into T_MAP values(3, 3)");
    conn.executeSql("insert into T_MAP values(4, 4)");
    conn.executeSql("insert into T_MAP values(5, 5)");
    conn.executeSql("insert into T_MAP values(1, 6)");  // T_MAP.EMP_ID は必ず
    conn.executeSql("insert into T_MAP values(2, 7)");  // T_EMP.ID に存在するが、
    conn.executeSql("insert into T_MAP values(3, 8)");  // T_MAP.JOB_ID は
    conn.executeSql("insert into T_MAP values(4, 9)");  // T_JOB.ID になくてもかまわない
    conn.executeSql("insert into T_MAP values(5, 10)");

  }
  //  後に続くcleanupのためにselector/readerを解放すべく、{}で囲んだ
  {
    RWDBSelector select = dataBase.selector();
    select << tab1["NAME"] << tab2["NAME"];

    // 冒頭のコメントに書かれたSQLと等価な条件式を作る
    RWDBPhraseBook phrase = dataBase.phraseBook();
    RWDBExprFormDefinition on("on %0 %1 %2");
    RWDBExprFormDefinition RJOneArg = ("%0 RIGHT JOIN ");
    RWDBExprFormDefinition IJTwoArg = ("%0 INNER JOIN %1 ");
    RWDBExpr eq("=", FALSE);
    RWCString cs =  RJOneArg(tab2).asString(phrase) + "("
            + IJTwoArg(tab1,tab3).asString(phrase)
            + on(tab1["ID"], eq,tab3["EMP_ID"]).asString(phrase) + ") "
            + on(tab2["ID"], eq,tab3["JOB_ID"]).asString(phrase);

    select.from(cs);

    //  RDBに送られるSQLを出力
    cout << select.asString() << endl;
    // SELECT 実行!
    RWDBResult restab = select.execute();

    // 結果を出力
    RWDBReader rdr = restab.table().reader();

    RWDBValue in1;
    RWDBValue in2;
    while(rdr())
    {
      rdr >> in1 >> in2;
      cout << in1.asString() << "\t" << in2.asString() << endl;
    }

  }
  // テーブルを削除
  tab1.drop();
  tab2.drop();
  tab3.drop();
  return 0;
}

DBTools.h++ vs Oracle-OCI/ODBC

TABLE1
フィールド名
ID Integer
Name String
Percent Float

上のようなテーブルを読み出すコードをDBTools.h++ と Oracle-OCI,そしてODBCで書かれたコードを示します。DBTools.h++がどれだけ簡単でパワpフルかがわかります。

/* * DBTools.h++ による * 'SELECT * FROM TABLE1' */

#include <iostream>
#include <rw/db/db.h>

using namespace std;

int       anInt;
RWCString aString;
float     aFloat;

void main () {
  // データベースをオープン
  RWDBDatabase aDB =
    RWDBManager::database("ODBC", "Q+E_paradox", "", "", "c:\\paradat");
  // テーブルを取得
  RWDBTable aTable = aDB.table("TABLE1");
  // selectorを取得
  RWDBSelector aSelector = aDB.selector();
  // SELECT * FROM
  aSelector << aTable;
  // readerを取得
  RWDBReader aReader = aSelector.reader();
  // 全レコードを出力
  while (aReader()) {
    aReader >> anInt >> aString >> aFloat;
    cout << anInt << '\t' << aString << '\t' << aFloat << endl;
  }
}
/* Oracle OCI による
 * 'SELECT * FROM TABLE1'
 */
#include <stdio.h>
#include <ctype.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#include <oci.h>
#include <iostream.h>

/* カーソル */

struct cda_def cursorStruct;
struct cda_def *cursor = &cursorStruct;

#define ASTRING_LEN 31

int   anInt;
char  aString[ASTRING_LEN];
float aFloat;

char *selectString = "SELECT * FROM TABLE1";

Lda_Def lda;
text hda[256];

int getOff() {
  ologof(&lda);
  return -1;
}

int main(int argc, char **argv) {
  int ret;
  char *uid = "SCOTT/TIGER";

  /* Oracle にログイン */
  if (orlon(&lda, hda, (text *) uid, -1, 0, -1, -1))
    return getOff();

  /* カーソルオープン */
  if (oopen(cursor, &lda, (text *) 0, -1, -1, (text *) 0, -1))
    return getOff();

  /* SQLを実行 */
  if (osql3(cursor, (text *) selectString, -1))
    return getOff();

  /* 出力バッファにバインド */
  if (odefin(cursor, 1, (ub1 *) &anInt, sizeof(int), OCI_EINT,
             -1, (sb2 *) 0, (text *) 0, -1, -1, (ub2 *) 0, (ub2 *) 0)
      || odefin(cursor, 2, (ub1 *) aString, ASTRING_LEN, OCI_ESTR,
             -1, (sb2 *) 0, (text *) 0, -1, -1, (ub2 *) 0, (ub2 *) 0)
      || odefin(cursor, 3,(ub1 *) &aFloat, sizeof(float), OCI_EFLT,
             -1, (sb2 *) 0, (text *) 0, -1, -1, (ub2 *) 0, (ub2 *) 0))
    return getOff();

  if (oexec(cursor))
    return getOff();

  /* カーソルを移動しながら出力 */
  while (!ofetch(cursor))
    cout << anInt << '\t' << aString << '\t' << aFloat << endl;

  ologof(&lda);
  return 0;
}
/*
 * ODBC による
 * 'SELECT * FROM TABLE1'
 */
#include <windows.h>
#include <odbcinst.h>
#include <w16macro.h>
#include <sql.h>
#include <sqlext.h>
#include <iostream.h>

#define STRING_LEN 30

HENV    henv;
HDBC    hdbc;
HSTMT   hstmt;
RETCODE retcode;

int   anInt;
UCHAR aString[STRING_LEN];
float aFloat;
SDWORD cbInt, cbString, cbFloat;

void main() {
  /* 環境ハンドルを取得 */
  retcode = SQLAllocEnv(&henv);

  if (retcode != SQL_SUCCESS)
     return; /* Error */

  /* 接続ハンドルを取得 */
  retcode = SQLAllocConnect(henv, &hdbc);

  if (retcode != SQL_SUCCESS)
    return; /* Error */

  /* ログイン・タイムアウトを5秒に設定 */
  SQLSetConnectOption(hdbc, SQL_LOGIN_TIMEOUT, 5);
  /* データソースに接続 */
  retcode = SQLConnect(hdbc, "Q+E_paradox", SQL_NTS, "", SQL_NTS, "", SQL_NTS);

  if (retcode != SQL_SUCCESS && retcode != SQL_SUCCESS_WITH_INFO)
    return; /* Error */

  /* ステートメントハンドルを取得 */
  retcode = SQLAllocStmt(hdbc, &hstmt);
  /* SQL文の実行 */
  retcode = SQLExecDirect(hstmt, "SELECT * FROM TABLE1", SQL_NTS);

  if (retcode == SQL_SUCCESS) {
    while (TRUE) {
      /* フェッチ */
      retcode = SQLFetch(hstmt);

      if (retcode == SQL_ERROR)
        return; /* Error condition */

      if (retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO) {
        /* レコードの出力 */
        SQLGetData(hstmt, 1, SQL_C_DEFAULT, &anInt, 0, &cbInt);
        SQLGetData(hstmt, 2, SQL_C_CHAR, aString, STRING_LEN, &cbString);
        SQLGetData(hstmt, 3, SQL_C_FLOAT, &aFloat, 0, &cbFloat);
        cout << anInt << '\t' << aString << '\t' << aFloat << endl;
      }
      else
        break;
    }
  }

  SQLFreeStmt(hstmt, SQL_DROP);
  SQLDisconnect(hdbc);
  SQLFreeConnect(hdbc);
  SQLFreeEnv(henv);
}