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

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

やっぱりSQLを書きたいんだ…

[part-1] , [part-2]と、DBTools.h++の簡単なチュートリアルを紹介しました。

DBTools.h++ は Oracle, MS SQL ServerなどのメージャなRDBをC++から操作するクラスライブラリです。

DBTools.h++は、RDBには付きもののSQLをコード中に記述せず、その代わりにデータベース,テーブルそして各種コマンド等をクラスの形で抽象化しています。そしてその抽象化されたオブジェクトに対する操作と、それに対応したSQL文との相互変換をおこなってくれます。

これにより、RDBによってちょっとづつ異なるSQL文を直接書き下すことなしに、単一のインタフェースでRDBを操作できるわけです。

しかしながら、異なるSQLをサポートしている各種RDBに対して単一のインタフェースを提供するのですから、特定のRDBのみに許された操作をDBTools.h++を介して行なうのにはどうしても限界があります。

DBTools.h++ version 4 以降、SQL文の直接実行(低レベル・インタフェース)を許す"Open SQL API"が新たに加わりました。Open SQLによって、プログラマは各種RDBがサポートするSQL文をC++コード中に直接書き下すことができるようになります。

DBTools.h++の高度に抽象化されたクラス群を駆使してRDBを操作するのもいいけれど、SQLに慣れ親しんだプログラマにとっては、Open SQLは"なんで今までなかったんだろう"と言いたくなるような機能でしょう。

ただし、SQL文を直接実行するのですから、SQL文によってはRDBを交換すると正常に動作しないことも起こり得ます。DBTools.h++のメリットのひとつである"RDBに依存しない、統一オペレーション"を棄てることになります。

とはいえ、従来のDBTools.h++による操作と、新たに追加されたOpen SQL APIとは、ひとつのアプリケーション/ルーチン内で何の問題もなく共存できますから、今までに書かれたコードをOpen SQLを使うということのためにすべてを書き直す必要はありません。

Open SQL による SQLの実行

Open SQLを利用するには、まずデータベースと接続し、connectionを取得しなければなりません。とはいえ、Open SQLの為の特別なインタフェースがあるわけではなく、従来のコードとまったく同じです。

// データベースに接続
RWDBDatabase db = RWDBManager::database(
                       "msql12d.dll", // serverType
                       "trial",   // serverName
                       "sa",      // userName
                       "",        // password
                       ""         // databaseName
                       );
RWDBConnection connection = db.connection();

Open SQLでのSQL文を抽象化したクラス RWDBOsqlのインスタンスを定義します。

RWDBOSql sql;

Open SQLでのSQL実行はきわめて単純。RWDBOsqlのメソッドstatement()の引数にSQL文を与え、メソッドexecute()で実行します。execute()の引数にはデータベースから取得したconnectionを与えます。

cout << "テーブル'person' を削除します\n";
sql.statement("DROP TABLE person");
sql.execute(connection);

cout << "テーブル'person' を作成します\n";
sql.statement("CREATE TABLE person ("
                "name  varchar(50) NOT NULL,"
                "phone varchar(50) NOT NULL "
                ")");
sql.execute(connection);

RWDBOSql::statement()に与えるSQL文は、接続しているRDBが受理できるものでなくてはなりません。たとえば"SELECT * FROM foo WHERE a % b = 10"には剰余演算子'%'が使われており、SQL Serverでは問題ありませんが剰余演算子をサポートしないOracleではエラーとなります。

SQL文の実行によって発生するエラーは、execute()に引き続いてメソッドisValid()をコールすることで検出できます。

sql.statement(SQL文);
sql.execute(connection);
if ( !sql.isValid() ) { // 実行時のエラーを検出する
  cout << sql.status().message() << endl;
}

レコードの追加

Open SQLを使えば、接続しているRDBが許すすべてのSQL文が実行できるのだから、わざわざレコードの追加について説明する必要はないように思われることでしょう。

sql.statement("INSERT INTO person VALUES ( 'police','110'");
sql.execute(connection);
sql.statement("INSERT INTO person VALUES ( 'fire',  '119'");
sql.execute(connection);
....

嬉しいことにRogue Waveはパラメータ化したSQL文をサポートすることで複数レコードの一括挿入を可能にしてくれました

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-1",  "060-XXX-XXXX" },
  { "friend-2",  "070-XXX-XXXX" },
  { "s34",       "06-XXXX-XXXX" },
};

RWDBTBuffer<RWCString> name(10);
RWDBTBuffer<RWCString> phone(10);
sql.statement("INSERT INTO person VALUES ( ?, ? )");
sql << name << phone;
for ( int i = 0; i < 10; ++i ) {
  name[i]  = entries[i].name;
  phone[i] = entries[i].phone;
}
sql.execute(connection);
dump(person);

上のコードでは、まず10個分の文字列を格納できるバッファ name と phoneを用意します。
次にSQL文を "INSERT INTO person VALUES ( ?, ? )"とします。SQL文中にある'?'が可変部分です。

※ SQL ServerやODBCでは'?'が用いられますが、他のRDBでは異なります

そして、sql << name << phoneによって可変部分と文字列バッファがバインドされます。後は挿入したいレコードのデータをバッファに設定し、execute()で実行します。

レコードの読み出し

最も使用頻度の高い"SELECT"についても、上記の"複数レコード一括挿入"と同様、検索結果の一括読み出しができるように作られています。

sql.statement("SELECT name, phone FROM person WHERE phone LIKE '11%'");
sql.execute(connection);
RWDBMultiRow result(sql.schema(), 3);
sql[0] = result;
int rows;
do {
  sql.fetch();
  rows = sql.rowsFetched();
  for ( int i = 0; i < rows; ++i ) {
    cout << setw(20) << result[i][0].asString()
         << setw(14) << result[i][1].asString()
         << endl;
  }
} while ( rows > 0 );

SQL文 "SELECT name, phone FROM person WHERE phone LIKE '11%'" を実行することで'11'から始まる電話番号を持つレコードのnameとphoneを検索します。

次の RWDBMultiRow result(sql.schema(), 3); は、検索結果のスキーマ(nameとphone)をsqlから取得し、そのスキーマに適した(3個分のレコードが格納できる)バッファresultを作っています。
そして sql[0] = result; によって検索結果をresultとバインドします。

次に sql.fetch(); で、検索結果を(バインドされた)バッファに取り込みます。
sql.rowsFetched() が取り込んだレコード数を返しますから、その回数だけバッファから読み出して coutに出力しています。
result[x][y]によって第xレコード/第yフィールドの値 (RWDBValue)が取得できるので、asString()メソッドで文字列に変換します。

RWDBMultiRowをもちいたこの方法は、SQL文の実行結果がいかなるスキーマとなろうが、それに応じたバッファを生成しますから非常に汎用的といえます。
しかしその反面、スキーマを基にバッファを作るのでパフォーマンス的には好ましくありません。

この例では検索結果はnameとphoneのふたつのフィールドであることが既知ですから、(一括書き込みに用いたバッファである)RWTBuffer<RWCString> を流用したコードに置き換えることができます。

sql.statement("SELECT name, phone FROM person WHERE phone LIKE '11%'");
ql.execute(connection);
sql[0] >> name >> phone;
int rows;
do {
  sql.fetch();
  rows = sql.rowsFetched();
  for ( int i = 0; i < rows; ++i ) {
    cout << setw(20) << name[i]
         << setw(14) << phone[i]
           << endl;
    }
  } while ( rows > 0 );

RWDBMultiRowとは異なり、>>でバインドしているところがちょっと違う程度ですね。

/*
 * 'まとめ'のお試しコード
 */
#include <iostream>         // cout, endl
#include <iomanip>          // setw
#include <rw/db/db.h>       // most of DBTools.h++ stuff
#include <rw/db/tbuffer.h>  // RWDBTBuffer<T>

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

using namespace std;

void dump(RWDBTable& table) {
  RWDBReader reader = table.reader();
  int count = 0;
  RWCString name, phone;
  while ( reader() ) {
    reader >> name >> phone;
    cout << setw(20) << name
         << setw(14) << phone
         << endl;
    ++count;
  }
  cout << count << " records.\n\n";
}

// MS SQL Server と接続する multithread StdLib shared
#ifdef RWDEBUG
#define DB_SERVER "msq15d.dll"
#else
#define DB_SERVER "msq12d.dll"
#endif

int trial() {

  // データベースに接続
  cout << "データベース'trial'に接続します\n";
  RWDBDatabase db = RWDBManager::database(
                       DB_SERVER, // serverType
                       "trial",   // serverName
                       "sa",      // userName
                       "",        // password
                       ""         // databaseName
                       );

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

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

  RWDBConnection connection = db.connection();
  RWDBTable person = db.table("person");
  RWDBOSql sql;

  if ( person.exists(true) ) {
    cout << "テーブル'person' を削除します\n";
    sql.statement("DROP TABLE person");
    sql.execute(connection);
  }

  cout << "テーブル'person' を作成します\n";
  sql.statement("CREATE TABLE person ("
                  "name  varchar(50) NOT NULL,"
                  "phone varchar(50) NOT NULL "
                ")");
  sql.execute(connection);

  cout << "レコードをテーブルに追加します\n";
  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-1",  "060-XXX-XXXX" },
    { "friend-2",  "070-XXX-XXXX" },
    { "s34",       "06-XXXX-XXXX" },
  };

  RWDBTBuffer<RWCString> name(10);
  RWDBTBuffer<RWCString> phone(10);
  sql.statement("INSERT INTO person VALUES ( ?, ? )");
  sql << name << phone;
  for ( int i = 0; i < 10; ++i ) {
    name[i]  = entries[i].name;
    phone[i] = entries[i].phone;
  }
  sql.execute(connection);
  dump(person);

  // レコードの削除
  cout << "電話番号が'06'で始まるレコードを削除します\n";
  sql.statement("DELETE FROM person WHERE phone LIKE '06%'");
  sql.execute(connection);
  dump(person);

  // レコードの更新
  cout << "'weather'を'forecast'に変更します\n";
  sql.statement("UPDATE person SET name = 'forcast' WHERE name = 'weather'");
  sql.execute(connection);
  dump(person);

  // 条件によるレコードの読み出し 
  cout << "電話番号が'11'から始まるレコードは:\n";
  sql.statement("SELECT name, phone FROM person WHERE phone LIKE '11%'");
  sql.execute(connection);
#if 1
  RWDBMultiRow result(sql.schema(), 3);
  sql[0] = result;
  int rows;
  do {
    sql.fetch();
    rows = sql.rowsFetched();
    for ( int i = 0; i < rows; ++i ) {
      cout << setw(20) << result[i][0].asString()
           << setw(14) << result[i][1].asString()
           << endl;
    }
  } while ( rows > 0 );
#else
  sql[0] >> name >> phone;
  int rows;
  do {
    sql.fetch();
    rows = sql.rowsFetched();
    for ( int i = 0; i < rows; ++i ) {
      cout << setw(20) << name[i]
           << setw(14) << phone[i]
           << endl;
    }
  } while ( rows > 0 );
#endif

  return 0;
}

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

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