Transactd チュートリアル

はじめに

対象読者

このチュートリアルは、Transactdに初めて触れる読者が、実際にコードを動かしながら基本的な概念とAPIの使い方を習得できるよう書かれています。

SQLの知識は必要ありませんが、MySQL/MariaDBをインストールし、コマンドラインクライアントを利用してコマンドを実行できる必要があります。

目次

プラグインとクライアントの準備

Transactdは、MySQL/MariaDBのプラグインとしてサーバー側で動作する「Transactd プラグイン」と、クライアント側のプログラムから利用する「Transactd クライアント」で構成されます。この章ではこれらのセットアップ手順を案内します。

プラグインとクライアントのインストール

MySQL/MariaDBとTransactd プラグイン

Transactd プラグインは、MySQL/MariaDBのプラグインとして動作します。インストール済のMySQL/MariaDBに対してプラグインをインストールします。

MySQL/MariaDBインストールについては、それぞれのプロダクトのドキュメントを参照してください。

Transactd プラグインのインストールについては、インストールガイドを参照してください。

また、インストール状態を確認するために、Transactd 運用マニュアルのTransactd プラグインの状態の確認を行ってください。

Transactd クライアント

現在、以下のプログラミング言語用のTransactd クライアントが用意されています。

言語ごとにインストール方法を説明します。

C++用クライアントは、使用するコンパイラがLinuxのgccもしくはWindowsのVisual Studio 2010であれば、ビルド済みバイナリをインストールすることができます。 Linuxはx86-64Bit 、Windowsはx86-32Bit/64Bitの両方があります。LinuxはLSBに基づいた Genericなバイナリです。

それ以外のコンパイラの場合はソースコードからクライアントライブラリをビルドします。

詳しいインストール方法はインストールガイドを参照してください。

ビルド済みのCOMコンポーネントをインストールすることができます。

COMコンポーネントはWindows用のx86-32Bit/64Bitの両方が用意されています。

詳しいインストール方法はインストールガイドを参照してください。

Ruby用クライアントは、RubyGemsのパッケージとしてインストールすることができます。

詳しいインストール方法はインストールガイドを参照してください。

Linuxではソースコードからクライアントライブラリをビルドします。

詳しいインストール方法はインストールガイドを参照してください。

Windowsの場合、お使いのPHPに合わせてビルド済みバイナリをインストールすることができます。

ビルド済みのCOMコンポーネントをインストールすることができます。

COMコンポーネントはWindows用のx86-32Bit/64Bitの両方が用意されています。

詳しいインストール方法はインストールガイドを参照してください。

サンプルデータベースとテーブルの作成

すでにSQLを使って運用中のユーザー情報データベースに対して、Transactdでアクセスする方法を解説します。

コマンドラインクライアントなどから以下のSQLを発行し、ユーザー情報データベースtdtutorialを作成します。

CREATE DATABASE `tdtutorial`;
use tdtutorial;

CREATE TABLE `users` (`id` int NOT NULL, `name` varbinary(32) NOT NULL,
  `tel` varbinary(20) NOT NULL, `group` int NOT NULL, 
  PRIMARY KEY (`id`),index group_idx (`group`));

CREATE TABLE `groups` (`id` int NOT NULL, `name` varbinary(32) NOT NULL, PRIMARY KEY (`id`));

/* users  record */
INSERT INTO `users` VALUES
  (1, 'user1', '0123-456-xxx', 1),
  (2, 'user2', '9876-543-xxx', 2),
  (3, 'user3', '', 1),
  (4, 'user4', '080-xxxx-xxxx', 2),
  (5, 'user5', '090-yyyy-yyyy', 3),
  (6, 'user6', '090-zzzz-zzzz', 3),
  (7, 'user7', '', 3),
  (8, 'user8', '090-aaaa-bbbb', 3),
  (9, 'user9', '0123-456-xxx', 2),
  (10, 'user10', '0123-456-aaa', 3),
  (11, 'user11', '090-cccc-dddd', 3),
  (12, 'user12', '0123-456-bbb', 3),
  (13, 'user13', '0123-456-ccc', 2),
  (14, 'user14', '', 3),
  (15, 'user15', '0123-456-dddd', 3);

/* groups  record */
INSERT INTO `groups` VALUES
  (1, 'Administrators'),
  (2, 'DBOperators'),
  (3, 'Users'),
  (4, 'Guests');

このtdtutorialデータベースにはusersgroupsの2つのテーブルがあります。

プログラム言語ごとの準備

このチュートリアルでは、実際に動かせるサンプルコードを掲載しています。Transactdを使ったプログラムをビルドして動かす方法は言語ごとに異なります。

以下は、ソースコードのコンパイルとリンクを行うのに必要な情報です。

C++のプログラムをビルドする際に、Transactdのヘッダーファイルをincludeしライブラリファイルをリンクできるよう、以下のディレクトリを設定します。

Windowsの場合

  • TransactdのソースコードパッケージもしくはSDKのsourceディレクトリをincludeパスに加える。
    (例:C:\transactd-client-win64_with_sdk-2.4.3\source
  • BOOSTのパスをincludeパスに加える。
    (例:C:\boost\boost_1_58_0
  • tdclcpp_xxx_xx_xxx.libとリンクするために、SDKまたはビルドディレクトリのlibディレクトリを libraryパスに加える。
    (例:C:\transactd-client-win64_with_sdk-2.4.3\lib
  • tdclcpp_xxx_xx_xxx.libをリンクするようプロジェクトに加えるか、 Visual Studio および Embarcadero C++ Builder を使用している場合、プロジェクトのプロパティでプリプロセッサに TRDCL_AUTOLINKを設定する。

Linuxの場合

  • TransactdのソースコードパッケージもしくはSDKのsourceディレクトリをincludeパスに加える。
    (例:~/transactd-client-linux-x86_64_with_sdk-2.4.3/source
  • 同様にsource/linuxディレクトリもincludeパスに加える。
    (例:~/transactd-client-linux-x86_64_with_sdk-2.4.3/source/linux
  • BOOSTのパスをincludeパスに加える。
    (例:~/boost/boost_1_58_0
  • libtdclcpp_64m.so.2.4がインストールされているディレクトリをlibraryパスに加え、 tdclcpp_64mをリンクするようにMakefileを作成する。Makefileの例

JScriptのプログラムは、コンパイルする必要はありません。

Rubyのプログラムは、コンパイルする必要はありません。

PHPのプログラムは、コンパイルする必要はありません。

Visual Studioの[プロジェクト]メニューから[参照の追加]を選択し、[参照マネージャー]を開きます。[COM]のリストから transactdをチェックし、参照に追加します。

参照に追加すると[オブジェクトブラウザ]で、Transactdクライアントのすべての定数、関数、クラスの定義が確認できます。 それらの正しい名前を知りたいときは、これで確認すると便利です。

includeやrequire

言語ごとに、includeや名前空間の指定などを説明します。

SDKに含まれるヘッダーファイルとサンプルで使用する標準ライブラリのヘッダーを#includeします。

#include <bzs/db/protocol/tdap/client/database.h>
#include <bzs/db/protocol/tdap/client/table.h>
#include <stdio.h>

Transactdクライアントの名前空間を省略できるようにusing namespace宣言します。

using namespace bzs::db::protocol::tdap::client;
using namespace bzs::db::protocol::tdap;

JScriptでTransactdクライアントを使用する際には、requireやincludeのような記述は不要です。各クラスのインスタンスはnew ActiveXObjectで生成します。

var db = new ActiveXObject('transactd.database');

RubyGemsとしてインストールしたTransactdクライアントは、通常のgemと同様にrequireして使用できます。

require 'transactd'

PHPでは、transactd.phpinclude_pathに設定されたフォルダに配置してrequireにファイル名を指定するか、requireに直接transactd.phpのパスを指定する必要があります。 transactd.phpには、Transactdクライアントの定数定義や、より便利に使用するためのArrayAccessインターフェースの実装などが含まれています。

各種クラスが配置されている名前空間はBizStation\Transactdです。使用するクラスを事前にuse宣言します。

require('transactd.php');
use BizStation\Transactd\Transactd;
use BizStation\Transactd\Database;
use BizStation\Transactd\ActiveTable;
use BizStation\Transactd\Query;
use BizStation\Transactd\SortFields;
use BizStation\Transactd\GroupQuery;
use BizStation\Transactd\Count;
use BizStation\Transactd\RecordsetQuery;
use BizStation\Transactd\Tabledef;

transactd.phpにはTransactdクライアントのすべての定数、関数、クラスが定義されています。 また、引数と戻り値のPHPDocコメントが付けられています。PHPDocに対応するエディタであればコード補完が行えます。 そうでない場合は、このファイルを検索すると便利です。

Transactdクライアントの名前空間を省略できるようにusing宣言します。

using transactd;

データベースとテーブルの操作

データベースのOpenとClose

まずはじめにデータベースを開いてみましょう。

static const _TCHAR* uri = _T("tdap://localhost/tdtutorial?dbfile=transactd_schema");
database* db = database::create();
db->open(uri, TYPE_SCHEMA_BDF, TD_OPEN_NORMAL);
// ...
// close and release database object
database::destroy(db);
var TYPE_BDF = 0;
var OPEN_NORMAL = 0;
var URI = 'tdap://localhost/tdtutorial?dbfile=transactd_schema';
// open database
var db = new ActiveXObject('transactd.database');
db.Open(URI, TYPE_BDF, OPEN_NORMAL);
// ...
// close database
db.Close();

databaseオブジェクトのメモリの解放はCOMによって自動で行われます。 メモリ解放時にデータベースも閉じられますが、明示的にcloseを呼び出すようにしましょう。

uri = 'tdap://localhost/tdtutorial?dbfile=transactd_schema'
# open database
db = Transactd::Database.new()
db.open(uri, Transactd::TYPE_SCHEMA_BDF, Transactd::TD_OPEN_NORMAL)
# ...
# close database
db.close()

databaseオブジェクトのメモリの解放はRubyのGCによって自動で行われます。 メモリ解放時にデータベースも閉じられますが、明示的にcloseを呼び出すようにしましょう。

$uri = 'tdap://localhost/tdtutorial?dbfile=transactd_schema';
// open database
$db = new Database();
$db->open($uri, Transactd::TYPE_SCHEMA_BDF, Transactd::TD_OPEN_NORMAL);
// ...
// close database
$db->close();

databaseオブジェクトのメモリの解放はPHPのGCによって自動で行われます。 メモリ解放時にデータベースも閉じられますが、明示的にcloseを呼び出すようにしましょう。

const string uri = "tdap://localhost/tdtutorial?dbfile=transactd_schema";
// open database
Database db = new Database();
db.Open(uri, eSchemaType.schemaBdf, eOpenMode.normal);
// ...
// close database
db.Close();

databaseオブジェクトのメモリの解放はCOMによって自動で行われます。 メモリ解放時にデータベースも閉じられますが、明示的にcloseを呼び出すようにしましょう。

データベースを開くには、まずdatabaseオブジェクトを作成します。次にopenメソッドのパラメータにURIを渡して、データベースを開きます。closeで閉じます。

URIフォーマット

データベースを開く際に、パラメータで渡したURIのフォーマットについて説明します。

tdap://username@localhost/tdtutorial?dbfile=transactd_schema&pwd=password
‾‾‾‾   ‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾
scheme username    host    database        schema table        password
項目説明
tdapTransactdプロトコルを表します。
username MySQLユーザー名を指定します。(ユーザーとホストの組み合わせ認証時のみ。デフォルトでは必要ありません。)
hostホスト名またはIPアドレスを指定します。
databaseデータベース名を指定します。
schema tabledbfile=でスキーマテーブルを指定します。 transactd_schemaと指定すると、スキーマテーブルを自動生成します。
Transactd 3.0以降では、スキーマテーブルの指定を省略し、スキーマ情報を自動取得することもできるようになりました。
password pwd=でパスワードを指定します。(ユーザーとホストの組み合わせ認証時のみ。デフォルトでは必要ありません。)

スキーマテーブルとは

スキーマテーブルは、データベース内にある全テーブルの構成情報を格納した、Transactdプラグイン用のメタ情報テーブルです。

スキーマテーブルは既存のデータベースから自動生成することができます。open時にスキーマテーブル名をtransactd_schemaにすると、データベースのテーブル情報を読み取って自動生成されます。

Transactd 3.0以降では、スキーマ情報をテーブルとして保存せず、毎回自動生成することもできるようになりました。

テーブルの構造をSQLで変更した場合

SQLのCREATE TABLEALTER TABLEを行いテーブルの構造を変更した場合、そのままでは、スキーマテーブルにはそれが反映されていません。 そのため、実際のテーブルの構造との間にミスマッチが発生します。

このような場合、スキーマテーブルを一度削除し、スキーマテーブル名にtransactd_schema を指定してデータベースを開くことで、最新の状態を反映したものにできます。

close(true /* withDropDefaultSchema */)と指定してデータベースを閉じると、transactd_schemaテーブルを削除することができます。

コネクション

databaseオブジェクトはTCPや名前付きパイプのコネクションも管理しています。openメソッドでは、既存の接続が無ければ自動で新しいコネクションを生成して使用します。そのため、通常はコネクションを意識する必要はありません。

詳しい情報はSDKのマルチスレッドアプリケーションを参照してください。

テーブルのOpenとClose

では次にテーブルを開いてみましょう。

// ...
// open table
table* tb = db->openTable(_T("users"), TD_OPEN_NORMAL);
// ...
// release table
tb->release();
// ...
// close and release database object
database::destroy(db);
// ...
// open table
var tb = db.OpenTable('users', OPEN_NORMAL);
// ...
// close table
tb.Close();
// ...
// close database
db.Close();
# ...
# open table
tb = db.openTable('users', Transactd::TD_OPEN_NORMAL)
# ...
# close table
tb.close()
# ...
# close database
db.close()
// ...
// open table
$tb = $db->openTable('users', Transactd::TD_OPEN_NORMAL);
// ...
// close table
$tb->close();
// ...
// close database
$db->close();
// ...
// open table
Table tb = db.OpenTable("users", eOpenMode.normal);
// ...
// close table
tb.Close();
// ...
// close database
db.Close();

テーブルを開くにはopenTableメソッド、閉じるにはcloseメソッドを使用します。 テーブルを閉じてからデータベースを閉じていることに注意してください。

成功失敗の判定は、databaseオブジェクトのstatで取得します。成功であればゼロが返ります。

Read

最初の読み取り

テーブルからデータを読み取る手順は以下の通りです。

  1. 検索に使用するテーブルのインデックスを番号で指定します。
  2. そのインデックスのフィールドに、キーとなる値をセットします。
  3. seek系のメソッドを実行します。
  4. statで読み取り結果を判定します。成功するとゼロが返ります。
// read a record
tb->setKeyNum(0);
tb->clearBuffer();
tb->setFV(_T("id"), 2);
tb->seek();
if (tb->stat() == 0)
  _tprintf(_T("id = 2, name = %s\n"), tb->getFVstr(_T("name")));
else
  _tprintf(_T("Can not read id = 2"));
  • 値のセットはsetFVです。最初のパラメータがフィールド名、2番目が値です。値の型は_TCHAR* int64 int short double float などのオーバーロード関数が用意されています。
  • 値の取出しはgetFVxxxです。xxxの部分は取出す型に応じてgetFVint getFV64 getFVstr getFVdbl getFVbinなどが用意されています。
// read a record
tb.KeyNum = 0;
tb.ClearBuffer();
tb.Vlng('id') = 2;
tb.Seek();
if (tb.Stat == 0)
  WScript.Echo('id = 2, name = ' + tb.Text('name'));
else
  WScript.Echo('Can not read id = 2');
  • 型に応じて用意されたV64Vlng Vbin Text などの引数付きプロパティを使用して、値をセットもしくは取得します。
# read a record
tb.setKeyNum(0)
tb.clearBuffer()
tb.setFV('id', 2)
tb.seek()
if (tb.stat() == 0)
  puts('id = 2, name = ' + tb.getFVstr('name'))
else
  puts('Can not read id = 2')
end
  • 値のセットはsetFVです。最初のパラメータがフィールド名、2番目が値です。
  • 値の取出しはgetFVxxxです。xxxの部分は取出す型に応じてgetFVint getFVstr getFVbin などが用意されています。
// read a record
$tb->setKeyNum(0);
$tb->clearBuffer();
$tb->setFV('id', 2);
$tb->seek();
if ($tb->stat() == 0)
  echo('id = 2, name = ' . $tb->getFVstr('name') . PHP_EOL);
else
  echo('Can not read id = 2' . PHP_EOL);
  • 値のセットはsetFVです。最初のパラメータがフィールド名、2番目が値です。
  • 値の取出しはgetFVxxxです。xxxの部分は取出す型に応じてgetFVint getFVstr getFVbin などが用意されています。
// read a record
tb.KeyNum = 0;
tb.ClearBuffer();
tb.Vlng["id"] = 2;
tb.Seek();
if (tb.Stat == 0)
  Console.WriteLine("id = 2, name = " + tb.Text["name"]);
else
  Console.WriteLine("Can not read id = 2");
  • 型に応じて用意されたV64Vlng Vbin Text などの引数付きプロパティを使用して、値をセットもしくは取得します。

これで、指定したレコードを検索しデータを読み取ります。この読み取ったレコード(行)がカレント行です。 キー値で指定したレコードが無い場合はカレント行は不定状態になります。

ここで使用したメソッドなどを補足します。

メソッド該当する演算子と取得するレコード
seek()=キー値が指定した値と一致したレコード
seekGreater(OR_EQUAL = true)>= キー値が指定した値以上の最初のレコード
seekGreater(OR_EQUAL = false)> キー値が指定した値より大きい最初のレコード
seekLessThan(OR_EQUAL = true)<= キー値が指定した値以下の最初のレコード
seekLessThan(OR_EQUAL = false)< キー値が指定した値より小さい最初のレコード
seekFirst()インデックス順で最初のレコード
seekLast()インデックス順で最後のレコード

サーバーカーソル

次に行の移動などを行いますが、その前に大事な概念を説明します。

Transactdのtableクラスを使用したデータアクセスのカレント行は、基本的にはサーバー側で管理されます。たとえばnstable::seekNext()メソッドは、サーバー側で管理されたカレント行を次の行に移動して、そのデータをクライアントへ送信します。このようにサーバー側で管理されるカーソルをサーバーカーソルと呼びます。

行の移動

カレント行を確定すると、そこからseekPrevまたはseekNextで前後へ1レコードずつ移動することができます。

seekFirstとseekNextで全レコードを読み取る

usersテーブルのすべてのレコードを読み取ってみましょう。

// read all records
tb->seekFirst();
while (tb->stat() == 0)
{
  _tprintf(_T("id = %ld name = %s\n"), tb->getFVint(_T("id")) , tb->getFVstr(_T("name")));
  tb->seekNext();
}
// read all records
tb.SeekFirst();
while (tb.Stat == 0)
{
  WScript.Echo('id = ' + tb.Vlng('id') + ', name = ' + tb.Text('name'));
  tb.SeekNext();
}
# read all records
tb.seekFirst()
while (tb.stat() == 0) do
  puts('id = ' + tb.getFVint('id').to_s() + ', name = ' + tb.getFVstr('name'))
  tb.seekNext()
end
// read all records
$tb->seekFirst();
while ($tb->stat() == 0)
{
  echo('id = ' . $tb->getFVint('id') . ', name = ' . $tb->getFVstr('name') . PHP_EOL);
  $tb->seekNext();
}
// read all records
tb.SeekFirst();
while (tb.Stat == 0)
{
  Console.WriteLine("id = " + tb.Vlng["id"] + ", name = " + tb.Text["name"]);
  tb.SeekNext();
}

seekGreaterで id >= 2 and id <= 5 のデータを読み取る

次に、idが2~5の範囲を読み取ってみましょう。

// select records id >= 2 and id <= 5
const static int OR_EQUAL = true;
tb->clearBuffer();
tb->setFV(_T("id"), 2);
tb->seekGreater(OR_EQUAL);
while (tb->stat() == 0)
{
  if (tb->getFVint(_T("id")) > 5) 
    break;
  _tprintf(_T("id = %ld, name = %s\n"), tb->getFVint(_T("id")), tb->getFVstr(_T("name")));
  tb->seekNext();
}
// select records id >= 2 and id <= 5
var OR_EQUAL = true;
tb.ClearBuffer();
tb.Vlng('id') = 2;
tb.SeekGreater(OR_EQUAL);
while (tb.Stat == 0)
{
  if (tb.Vlng('id') > 5)
    break;
  WScript.Echo('id = ' + tb.Vlng('id') +  ', name = ' + tb.Text('name'));
  tb.SeekNext();
}
# select records id >= 2 and id <= 5
or_equal = true
tb.clearBuffer()
tb.setFV('id', 2)
tb.seekGreater(or_equal)
while (tb.stat() == 0) do
  break if (tb.getFVint('id') > 5)
  puts('id = ' + tb.getFVint('id').to_s() + ', name = ' + tb.getFVstr('name'))
  tb.seekNext()
end
// select records id >= 2 and id <= 5
const OR_EQUAL = true;
$tb->clearBuffer();
$tb->setFV('id', 2);
$tb->seekGreater(OR_EQUAL);
while ($tb->stat() == 0)
{
  if ($tb->getFVint('id') > 5)
    break;
  echo('id = ' . $tb->getFVint('id') . ', name = ' . $tb->getFVstr('name') . PHP_EOL);
  $tb->seekNext();
}
// select records id >= 2 and id <= 5
var OR_EQUAL = true;
tb.ClearBuffer();
tb.Vlng('id') = 2;
tb.SeekGreater(OR_EQUAL);
while (tb.Stat == 0)
{
  if (tb.Vlng["id"] > 5)
    break;
  Console.WriteLine("id = " + tb.Vlng["id"] + ", name = " + tb.Text["name"]);
  tb.SeekNext();
}

seekNextで読み取るレコードの並びはindexで指定された順なので、id順であることが保証されています。あとはループの中で条件をコードで判断するだけで、とても簡単です。

Insert

テーブルにレコードを追加する手順は以下の通りです。

  1. 最初に clearBuffer()を呼び出してすべてのフィールドの値をゼロまたは空文字""に初期化します。
  2. 次にtableオブジェクトに必要なフィールドの値を設定してからinsertメソッドを呼び出します。

それでは実際に追加してみます。

// Insert user16
tb->clearBuffer();
tb->setFV(_T("id"), 16);
tb->setFV(_T("name"), _T("user16"));
tb->setFV(_T("tel"), _T("0123-abcd-efgh"));
tb->setFV(_T("group"), 3);
tb->insert();
if (tb->stat() != 0)
  _tprintf(_T("\n Can not insert user16, stat = %d\n"), tb->stat());
else
  _tprintf(_T("\n user16 insert success.\n"));
// Insert user16
tb.ClearBuffer();
tb.Vlng('id') = 16;
tb.Text('name') = 'user16';
tb.Text('tel') = '0123-abcd-efgh';
tb.Text('group') = 3;
tb.Insert();
if (tb.Stat != 0)
  WScript.Echo('Can not insert user16, stat = ' + tb.Stat);
else
  WScript.Echo('user16 insert success.');
# Insert user16
tb.clearBuffer()
tb.setFV('id', 16)
tb.setFV('name', 'user16')
tb.setFV('tel', '0123-abcd-efgh')
tb.setFV('group', 3)
tb.insert()
if (tb.stat() != 0)
  puts('Can not insert user16, stat = ' + tb.stat().to_s())
else
  puts('user16 insert success.')
end
// Insert user16
$tb->clearBuffer();
$tb->setFV('id', 16);
$tb->setFV('name', 'user16');
$tb->setFV('tel', '0123-abcd-efgh');
$tb->setFV('group', 3);
$tb->insert();
if ($tb->stat() != 0)
  echo('Can not insert user16, stat = ' . $tb->stat() . PHP_EOL);
else
  echo('user16 insert success.');
// Insert user16
tb.ClearBuffer();
tb.Vlng["id"] = 16;
tb.Text["name"] = "user16";
tb.Text["tel"] = "0123-abcd-efgh";
tb.Vlng["group"] = 3;
tb.Insert();
if (tb.Stat != 0)
  Console.WriteLine("Can not insert id = 16, stat = " + tb.Stat);
else
  Console.WriteLine("user16 insert success.");

Update

既存のレコードを更新する手順は以下の通りです。

  1. 検索に使用するテーブルのインデックスを番号で指定します。
  2. そのインデックスのフィールドに、キーとなる値をセットします。
  3. seek系のメソッドを実行して更新するレコードを読み取ります。
  4. 必要なフィールドの値を変更してupdateメソッドを呼び出します。

それではid=2のレコードのnameフィールドをuserBに変更してみます。

// Update user2 to userB
tb->clearBuffer();
tb->setFV(_T("id"), 2);
tb->seek();
if (tb->stat() == 0)
{
  tb->setFV(_T("name"), _T("userB"));
  tb->update();
  if (tb->stat() != 0)
    _tprintf(_T("\n Can not update name id = 2 stat = %d\n"), tb->stat());
  else
    _tprintf(_T("\n user2 update success.\n"));
}
else
  _tprintf(_T("\n Can not read id = 2\n"));
// Update user2 to userB
tb.ClearBuffer();
tb.Vlng('id') = 2;
tb.Seek();
if (tb.Stat == 0)
{
  tb.Text('name') = 'userB';
  tb.Update();
  if (tb.Stat != 0)
    WScript.Echo('Can not update name id = 2 stat = ' + tb.Stat);
  else
    WScript.Echo('user2 update success.');
}
else
  WScript.Echo('Can not read id = 2');
# Update user2 to userB
tb.clearBuffer()
tb.setFV('id', 2)
tb.seek()
if (tb.stat() == 0)
  tb.setFV('name', 'userB')
  tb.update()
  if (tb.stat() != 0)
    puts('Can not update name id = 2 stat = ' + tb.stat().to_s())
  else
    puts('user2 update success.')
  end
else
  puts('Can not read id = 2')
end
// Update user2 to userB
$tb->clearBuffer();
$tb->setFV('id', 2);
$tb->seek();
if ($tb->stat() == 0)
{
  $tb->setFV('name', 'userB');
  $tb->update();
  if ($tb->stat() != 0)
    echo('Can not update name id = 2 stat = ' . $tb->stat() . PHP_EOL);
  else
    echo('user2 update success.');
}
else
  echo('Can not read id = 2' . PHP_EOL);
// Update user2 to userB
tb.ClearBuffer();
tb.Vlng["id"] = 2;
tb.Seek();
if (tb.Stat == 0)
{
  tb.Text["name"] = "userB";
  tb.UpDate();
  if (tb.Stat != 0)
    Console.WriteLine("Can not update name id = 2 stat = " + tb.Stat);
  else
    Console.WriteLine("user2 update success.");
}
else
  Console.WriteLine("Can not read id = 2");

updateはカレント行を指定した値で上書きします。カレント行が存在しないまま行うと、statSTATUS_NO_CURRENTが返ります。

また、読み取りと更新の間にそのレコードが別のクライアントによって変更または削除されていた場合は、コンフリクトを検出しstatSTATUS_CHANGE_CONFLICTが返ります。

updateの引数にchangeinkeyを指定すると、事前にカレントを移動することなくキー値でカレントを確立し更新することができます。(ユニークキーを指定し、キー値は更新しない場合)

Delete

既存のレコードを削除する手順は以下の通りです。

  1. 検索に使用するテーブルのインデックスを番号で指定します。
  2. そのインデックスのフィールドに、キーとなる値をセットします。
  3. seek系のメソッドを実行して削除するレコードを確定します。
  4. deleteメソッドを呼び出します。

それではid=16のレコードを削除してみます。

// Delete user16
tb->clearBuffer();
tb->setFV(_T("id"), 16);
tb->seek();
if (tb->stat() == 0)
{
  tb->del();
  if (tb->stat() != 0)
    _tprintf(_T("\n Can not delete id = 16, stat = %d\n"), tb->stat());
  else
    _tprintf(_T("\n user16 delete success.\n"));
}
else
  _tprintf(_T("\n Can not read id = 16\n"));
// Delete user16
tb.ClearBuffer();
tb.Vlng('id') = 16;
tb.Seek();
if (tb.Stat == 0)
{
  tb.Delete();
  if (tb.Stat != 0)
    WScript.Echo('Can not delete id = 16, stat = ' + tb.Stat);
  else
    WScript.Echo('user16 delete success.');
}
else
  WScript.Echo('Can not read id = 16');
# Delete user16
tb.clearBuffer()
tb.setFV('id', 16)
tb.seek()
if (tb.stat() == 0)
  tb.del()
  if (tb.stat() != 0)
    puts('Can not delete id = 16, stat = ' + tb.stat().to_s())
  else
    puts('user16 delete success.')
  end
else
  puts('Can not read id = 16')
end
// Delete user16
$tb->clearBuffer();
$tb->setFV('id', 16);
$tb->seek();
if ($tb->stat() == 0)
{
  $tb->del();
  if ($tb->stat() != 0)
    echo('Can not delete id = 16, stat = ' . $tb->stat() . PHP_EOL);
  else
    echo('user16 delete success.');
}
else
  echo('Can not read id = 16' . PHP_EOL);
// Delete user16
tb.ClearBuffer();
tb.Vlng["id"] = 16;
tb.Seek();
if (tb.Stat == 0)
{
  tb.Delete();
  if (tb.Stat != 0)
    Console.WriteLine("Can not delete id = 16, stat = " + tb.Stat);
  else
    Console.WriteLine("user16 delete success.");
}
else
  Console.WriteLine("Can not read id = 16");

deleteはカレント行を削除します。カレント行が存在しないまま行うと、statSTATUS_NO_CURRENTが返ります。

また、読み取りと削除の間にそのレコードが別のクライアントによって変更または削除されていた場合は、コンフリクトを検出しstatSTATUS_CHANGE_CONFLICTが返ります。

deleteの引数にinkey=trueを指定すると、事前にカレントを移動することなくキー値でカレントを確立し削除することができます。(ユニークキーを指定した場合)

Transaction

トランザクションを使って、複数レコードの更新をアトミックに行ってみましょう。

id=2id=3のユーザーの名前をupdateUserName関数を使って変更し、2人分をアトミックに更新します。

updateUserName(table* tb, int id, const _TCHAR* name)
{
  tb->setKeyNum(0);
  tb->clearBuffer();
  tb->setFV(_T("id"), id);
  tb->seek();
  if (tb->stat() == 0)
  {
    tb->setFV(_T("name"), name);
    tb->update();
  }
  return tb->stat();
}

/* Transaction */
db->beginTrn();
short ret = updateUserName(tb, 2, _T("John"));
if (ret == 0)
  ret = updateUserName(tb, 3, _T("Akira"));
if (ret == 0)
  db->endTrn();
else
  db->abortTrn();
function updateUserName(tb, id, name)
{
  tb.KeyNum = 0;
  tb.ClearBuffer();
  tb.Vlng('id') = id;
  tb.Seek();
  if (tb.Stat == 0)
  {
    tb.Text('name') = name;
    tb.Update();
  }
  return tb.Stat;
}

// Transaction
db.BeginTrn();
var ret = updateUserName(tb, 2, 'John');
if (ret == 0)
  ret = updateUserName(tb, 2, 'Akira');
if (ret == 0)
{
  db.EndTrn();
  WScript.Echo('Commit transaction.');
}
else
{
  db.AbortTrn();
  WScript.Echo('Rollback transaction.');
}
def updateUserName(tb, id, name)
  tb.setKeyNum(0)
  tb.clearBuffer()
  tb.setFV('id', id)
  tb.seek()
  if (tb.stat() == 0)
    tb.setFV('name', name)
    tb.update()
  end
  return tb.stat()
end

db.beginTrn()
ret = updateUserName(tb, 2, 'John')
if (ret == 0)
  ret = updateUserName(tb, 2, 'Akira')
end
if (ret == 0)
  db.endTrn()
  puts('Commit transaction.')
else
  db.abortTrn()
  puts('Rollback transaction.')
end
function updateUserName($tb, $id, $name)
{
    $tb->setKeyNum(0);
    $tb->clearBuffer();
    $tb->setFV('id', $id);
    $tb->seek();
    if ($tb->stat() == 0)
    {
        $tb->setFV('name', $name);
        $tb->update();
    }
    return $tb->stat();
}

// Transaction
$db->beginTrn();
$ret = updateUserName($tb, 2, 'John');
if ($ret == 0)
    $ret = updateUserName($tb, 2, 'Akira');
if ($ret == 0)
{
    $db->endTrn();
    echo('Commit transaction.' . PHP_EOL);
}
else
{
    $db->abortTrn();
    echo('Rollback transaction.' . PHP_EOL);
}
static private eStatus updateUserName(Table tb, int id, string name)
{
  tb.KeyNum = 0;
  tb.ClearBuffer();
  tb.Vlng["id"] = id;
  tb.Seek();
  if (tb.Stat == 0)
  {
    tb.Text["name"] = name;
    tb.UpDate();
  }
  return tb.Stat;
}

// Transaction
db.BeginTrn();
eStatus ret = updateUserName(tb, 2, "John");
if (ret == 0)
  ret = updateUserName(tb, 2, "Akira");
if (ret == 0)
{
  db.EndTrn();
  Console.WriteLine("Commit transaction.");
}
else
{
  db.AbortTrn();
  Console.WriteLine("Rollback transaction.");
}

beginTrnでトランザクションを開始し、endTrnでコミットします。abortTrnでロールバックできます。

トランザクションはdatabaseオブジェクトの単位で行われます。

トランザクション内での読み取りは、そのレコードが必ずロックされます。そのため、updatedeleteでコンフリクトが起こることはありません。 レコードロックに関するより詳しい情報は、SDKの「InnoDBのロックとその制御」またはブログ記事「TransactdでInnoDBロックを自在に操る」を参考にしてください。

エラー処理(database, table クラス)

これまで紹介したメソッドの処理結果はそれぞれのクラスのstatで取得します。 stat() == 0がtrueであれば成功、そうでなければ失敗を示します。 databasetableクラスのオペレーションは例外をスローしません。

stat()が返す値の意味は、SDKのエラーコード表にあります。また、statMsgメソッドを使うとエラーメッセージを取得できます。

_TCHAR errorMsg[1024] = { 0x00 };
if (db->stat() != 0)
{
  db->statMsg(errorMsg);
  _tprintf(_T("database erorr No.%d %s\n"), db->stat(), errorMsg);
}
if (db.Stat != 0)
{
  WScript.Echo("database erorr No." + db.Stat + " " + db.StatMsg());
}
if db.stat() != 0
  puts('database erorr No.' + db.stat().to_s() + ' ' + db.statMsg())
end
if ($db->stat() != 0)
{
  echo('database erorr No.' . $db->stat() . ' ' . $db->statMsg() . PHP_EOL);
}
if (db.Stat != 0)
{
  Console.WriteLine("database erorr:No.{0:d} {1}.", db.Stat, db.StatMsg());
}

ここまでのサンプルコードにエラー処理を加えたコードをダウンロードできます。

SQLライクな読取と結果セットの操作

このセクションで例示するサンプルコードは、解りやすくするためにエラー処理などを省略して記述してあります。 エラー処理を含んだ完全なサンプルはサンプルダウンロードからダウンロードしたコードを参照してください。

activeTableとは

activeTableクラスはSQLライクにデータにアクセスするためのクラスです。

これまで説明したtableクラスは1レコード単位でサーバーにアクセスしましたが、このactiveTableクラスは 1回のreadで複数のレコードを読み取ることができ、より高速な処理ができます。

queryクラスと併せて使用することで、SELECTWHEREといった表現を使用でき、SQLに近い感覚でデータの読み取りができます。

読み取ったデータの結果セットであるrecordsetは、OrderByMutchByGroupByといった演算を行うことができます。また、結果セットに対して異なるテーブルをJoinすることもできます。

activeTableでのread

activeTableを使用してデータを読み取る手順は以下の通りです。

  1. activeTableを生成します。
  2. 検索に使用するテーブルのインデックスを番号で指定します。
  3. そのインデックスのフィールドに、キーとなる値をセットします。
  4. queryオブジェクトを使って取得対象と検索終了条件を指定します。
  5. readメソッドを使って読み取ります。

今回はusersテーブルからgroupが2以下のユーザーを読み取ります。まずは手順1~3までを行ってみます。

database* db = database::create();
db->open(uri, TYPE_SCHEMA_BDF, TD_OPEN_NORMAL);

activeTable at(db, _T("users"));
at.index(1).keyValue(0).alias(_T("name"), _T("user_name"));
// ...
database::destroy(db);

keyValue関数はtable::setFVと同様に_TCHAR* int64 int short double float などのオーバーロード関数が用意されています。

var db = new ActiveXObject('transactd.database');
db.Open(URI, TYPE_BDF, OPEN_NORMAL);

var at = new ActiveXObject('transactd.activeTable');
at.SetDatabase(db, 'users');
at.Index(1).KeyValue(0).Alias('name', 'user_name');
// ...
db.Close();
db = Transactd::Database.new()
db.open(uri, Transactd::TYPE_SCHEMA_BDF, Transactd::TD_OPEN_NORMAL)

at = Transactd::ActiveTable.new(db, 'users')
at.index(1).keyValue(0).alias('name', 'user_name')
# ...
# MUST release ActiveTable object explicitly
at.release()
db.close()
$db = new Database();
$db->open($uri, Transactd::TYPE_SCHEMA_BDF, Transactd::TD_OPEN_NORMAL);

$at = new ActiveTable($db, 'users');
$at->index(1)->keyValue(0)->alias('name', 'user_name');
// ...
$db->close();
Database db = new Database();
db.Open(uri, eSchemaType.schemaBdf, eOpenMode.normal);

ActiveTable at = new ActiveTable();
at.SetDatabase(db, "users");
at.Index(1).KeyValue(0).Alias("name", "user_name");
// ...
db.Close();

条件がgroupなので、indexgroupに付けられたものを使用します。キー番号は作成順に0, 1, 2...と番号が振られるので、今回は1を指定します。

keyValueは検索を開始する位置で、通常は下限値を指定します。もし、マイナス値を含むようなら、下限値はINT_MIN値です。 今回は現実的な下限値である0を指定します。

aliasで、フィールドにエイリアス(別名)を付けることができます。

また、メソッドチェーンを行えるように、インデックス番号やキー値、エイリアスなどの値を設定するメソッドはatciveTable自身を返すようにデザインされています。

次に手順4のqueryオブジェクトを使った取得対象と検索終了条件の指定を行います。

query q;
q.select(_T("id"), _T("user_name"), _T("group"), _T("tel"))
 .where(_T("group"), _T("<="), 2).reject(1);

whereの3番目のvalueパラメータはtable::setFVと同様に_TCHAR* int64 int short double floatなどのオーバーロード関数が用意されています。

var q = new ActiveXObject('transactd.query');
q.Select('id', 'user_name', 'group', 'tel')
 .Where('group', '<=', 2).Reject(1);
q = Transactd::Query.new()
q.select('id', 'user_name', 'group', 'tel')
 .where('group', '<=', 2).reject(1)
$q = new Query();
$q->select('id', 'user_name', 'group', 'tel')
  ->where('group', '<=', 2)->reject(1);
QueryBase q = new QueryBase();
q.Select("id", "user_name", "group", "tel")
 .Where("group", "<=", 2).Reject(1);

selectで読み取るフィールドを列挙します。1回のselectで11個まで指定できます。それより多い場合は複数回selectを呼び出すことで追加できます。

条件はwhereを使って指定します。フィールド名、比較子、値の順で指定します。今回は使用していませんが、whereに続けてand_or_で複数の条件を指定することもできます。

reject(1)は、条件にマッチしないレコードが1つ見つかったら検索を中止するという意味です。 キーであるフィールドが条件になっているので、1つ目のアンマッチレコードgroup=3が現れたら即検索を中止します。 この条件をうまく使うと、無駄な検索を大幅に削減できます。 マッチしないレコードの数に関わらず検索を続ける場合はreject(0xFFFF)を指定します。

queryオブジェクトもactiveTableと同様に、値を設定するメソッドではqueryオブジェクト自身を返します。

最後に手順5readメソッドを使って読み取ります。読み取った結果はrecordsetとして返されます。ここまでをまとめると以下のようになります。

activeTable at(db, _T("users"));
at.index(1).keyValue(0).alias(_T("name"), _T("user_name"));
query q;
q.select(_T("id"), _T("user_name"), _T("group"), _T("tel"))
  .where(_T("group"), _T("<="), 2)
  .reject(1);
recordset rs;
at.read(rs, q);
var at = new ActiveXObject('transactd.activeTable');
at.SetDatabase(db, 'users');
at.Index(1).KeyValue(0).Alias('name', 'user_name');
var q = new ActiveXObject('transactd.query');
q.Select('id', 'user_name', 'group', 'tel')
  .Where('group', '<=', 2)
  .Reject(1);
var rs = at.Read(q);
at = Transactd::ActiveTable.new(db, 'users')
at.index(1).keyValue(0).alias('name', 'user_name')
q = Transactd::Query.new()
q.select('id', 'user_name', 'group', 'tel')
  .where('group', '<=', 2)
  .reject(1)
rs = at.read(q)
$at = new ActiveTable($db, 'users');
$at->index(1)->keyValue(0)->alias('name', 'user_name');
$q = new Query();
$q->select('id', 'user_name', 'group', 'tel')
  ->where('group', '<=', 2)
  ->reject(1);
$rs = $at->read($q);
ActiveTable at = new ActiveTable();
at.SetDatabase(db, "users");
at.Index(1).KeyValue(0).Alias("name", "user_name");
QueryBase q = new QueryBase();
q.Select("id", "user_name", "group", "tel")
  .Where("group", "<=", 2)
  .Reject(1);
Recordset rs = at.Read(q);

rsを表にするとこのような結果になります。

+---------------------------------------------------------------+
|id             |user_name      |group          |tel            |
+---------------+---------------+---------------+---------------+
|1              |user1          |1              |0123-456-xxx   |
|3              |Akira          |1              |               |
|2              |John           |2              |9876-543-xxx   |
|4              |user4          |2              |080-xxxx-xxxx  |
|9              |user9          |2              |0123-456-xxx   |
|13             |user13         |2              |0123-456-ccc   |
+---------------+---------------+---------------+---------------+

recordset

recordsetは行と列を表す2次元の配列に似たオブジェクトです。rs[1]["name"]のように列名でフィールドにアクセスすることが可能です。

recordsetの行と列をループしてデータを表示してみましょう。

// dump recordset
void dumpRecordset(recordset& rs)
{
  std::tcout.setf(std::ios::left, std::ios::adjustfield);
  const fielddefs& fds = *rs.fieldDefs();
  for(size_t i = 0; i < rs.size(); ++i)
  {
    std::tcout << _T("[") <<  i + 1 << _T("]\n");
    row& rec = rs[i];
    for(size_t col = 0; col < rec.size(); ++col)
        std::tcout << _T("\t") << std::setw(15) 
            << fds[col].name() 
            << rec[col].c_str()
            << std::endl;
  }
}

フィールドからの値の読み出しは、型に応じてc_str i64 d getBinなどが用意されています。

// dump recordset
function dumpRecordset(rs)
{
  // get field definitions
  var fd = rs.FieldDefs;
  // show records
  for (var i = 0; i < rs.Count; i++)
  {
    WScript.Echo('[' + i + ']');
    for (var j = 0; j < rs.Record(i).Size; j++)
      WScript.Echo('\t[' + fd.FieldDef(j).Name + ']' + rs.Record(i).Field(j).Text);
  }
}
# dump recordset
def dumpRecordset(rs)
  # get field definitions
  fd = rs.fieldDefs()
  # show records
  for i in 0...rs.size()
    puts '[' + i.to_s + ']'
    for j in 0...rs[i].size()
      puts '  [' + fd[j].name + ']' + rs[i][j].str()
    end
  end
end
// dump recordset
function dumpRecordset($rs)
{
    foreach ($rs as $row_id => $record)
    {
        echo('[' . $row_id . ']' . PHP_EOL);
        foreach ($record as $field_name => $field_value)
        {
            echo("\t" . '[' . $field_name . ']' . $field_value . PHP_EOL);
        }
    }
}
// dump recordset
static void dumpRecordset(Recordset rs)
{
  // get field definitions
  FieldDefs fds = rs.FieldDefs;
  // show records
  for (UInt32 i = 0; i < rs.Size; i++)
  {
    Console.WriteLine("[" + i + "]");
    Record r = rs[i];
    for (short j = 0; j < r.Size; j++)
      Console.WriteLine("\t[{0,-15}] {1}", fds[j].Name, r[j].Text);
  }
}

列情報は、fieldDefsメソッドでコレクション(fielddefs)を取得できます。 このコレクションでも[]を使って1つのフィールド情報にアクセスできます。

recordsetにはSQLのようなOrderBy GroupBy Union Joinといったメソッドが用意されています。順に説明します。

OrderBy

OrderByは結果セットを並べ替えるメソッドです。

recordsetidフィールドを基準に並び替えてみましょう。

rs.orderBy(_T("id"));
rs.OrderBy('id');
rs.orderBy('id')
$rs->orderBy('id');
rs.OrderBy("id");

もう少し複雑な、複数のフィールドで昇順と降順が混在するような並び替えをしてみましょう。

const static bool ASC = true;
const static bool DESC = false;
sortFields orders;
orders.add(_T("group"), ASC);
orders.add(_T("id"), DESC);
rs.orderBy(orders);
var ASC = true;
var DESC = false;
var orders = new ActiveXObject('transactd.sortFields');
orders.Add('group', ASC);
orders.Add('id', DESC);
rs.OrderByEx(orders);
ASC = true
DESC = false
orders = Transactd::SortFields.new()
orders.add('group', ASC)
orders.add('id', DESC)
rs.orderBy(orders)
const ASC = true;
const DESC = false;
$orders = new SortFields();
$orders->add('group', ASC);
$orders->add('id', DESC);
$rs->orderBy($orders);
const bool ASC = true;
const bool DESC = false;
SortFields orders = new SortFields();
orders.Add("group", ASC);
orders.Add("id", DESC);
rs.OrderByEx(orders);

sortFieldsクラスを使って、複数の条件(フィールド名と昇順/降順)を追加します。 最後にOrderByにそのオブジェクトを渡します。

Clone

SQLでは、結果セットの明細とその集計結果の両方を取得したい場合、①明細と集計でクエリーを2回発行するか、②明細を取得しプログラム上で集計処理を行うか、のどちらかをする必要があります。

Transactdでは、recordsetをコピーすることができます。まず結果セットを取得し、それをコピーしてから集計処理を行うことで、1回の読み取りで明細と集計結果の両方を取得できます。

取得済みのrsrs2にコピーしてみましょう。

recordset& rs2 = *rs.clone();
// ...
rs2.release();
var rs2 = rs.Clone();
rs2 = rs.clone()
$rs2 = clone $rs;

recordsetはマジックメソッド__clone()をサポートしているので、clone文を使用できます。

Recordset rs2 = rs.Clone();

GroupBy

GroupByでグルーピングを行う手順は以下の通りです。

  1. groupQueryを生成します。
  2. グルーピングに使用するKeyFieldを指定します。
  3. 計算関数と結果フィールドの名前を指定します。
  4. groupByメソッドを使ってグルーピングします。

groupフィールドの値でグループ化し、count計算関数でレコードの数を数え、user_countという名前の結果列に結果を格納してみましょう。

std::tcout << _T("\n groupBy group\n");
groupQuery gq;
gq.keyField(_T("group"));
count countFunc(_T("user_count"));
gq.addFunction(&countFunc);
rs2.groupBy(gq);
WScript.Echo('groupBy group');
var gq = new ActiveXObject('transactd.groupQuery');
gq.KeyField('group');
gq.AddFunction(1, null, 'user_count'); // eGroupFunc.fcount = 1
rs2.GroupBy(gq);
puts('groupBy group')
gq = Transactd::GroupQuery.new()
gq.keyField('group')
countFunc = Transactd::Count.new('user_count')
gq.addFunction(countFunc)
rs2.groupBy(gq)
echo('groupBy group');
$gq = new GroupQuery();
$gq->keyField('group');
$countFunc = new Count('user_count');
$gq->addFunction($countFunc);
$rs2->groupBy($gq);
Console.WriteLine("\ngroupBy group");
GroupQuery gq = new GroupQuery();
gq.KeyField("group");
gq.AddFunction(eGroupFunc.fcount, null, "user_count");
rs2.GroupBy(gq);

このような結果になります。

+---------------------+
|     group|user_count|
+----------+----------+
|         1|         2|
|         2|         4|
+----------+----------+

計算関数クラスには他にもsumavgmaxminなどがあります。

whenフィルタを指定して、レコードごとに計算関数の計算対象にするかを指定することもできます。

telが空のレコードを計算対象から除外してみましょう。

std::tcout << _T("\n groupBy with condition.\n");
rs2 = *rs.clone();
countFunc.when(_T("tel"), _T("<>") ,_T(""));
rs2.groupBy(gq);
WScript.Echo('groupBy with condition.');
var rq1 = new ActiveXObject('transactd.recordsetQuery');
rq1.When('tel', '<>', '');
gq.Reset();
gq.KeyField('group');
gq.AddFunction(1, null, 'user_count', rq1); // eGroupFunc.fcount = 1
rs2.GroupBy(gq);
puts('groupBy with condition.')
rs2 = rs.clone()
countFunc.when('tel', '<>', '')
rs2.groupBy(gq)
echo('groupBy with condition.');
$rs2 = $rs->clone();
$countFunc->when('tel', '<>', '');
$rs2->groupBy($gq);
Console.WriteLine("\ngroupBy with condition.");
RecordsetQuery rq1 = new RecordsetQuery();
rq1.When("tel", "<>", "");
gq.Reset();
gq.KeyField("group");
gq.AddFunction(eGroupFunc.fcount, null, "user_count", rq1);
rs2.GroupBy(gq);

group=1のユーザー数が1人減りました。

+---------------------+
|     group|user_count|
+----------+----------+
|         1|         1|
|         2|         4|
+----------+----------+

MatchBy

以下の手順で、結果セットを取得した後でもさらにWHEREのようなフィルタリングが行えます。

  1. recordsetQueryを生成します。
  2. フィルタリングの条件を指定します。
  3. MatchByメソッドを使ってフィルタリングします。

telが空白の行を結果セットから削除しましょう。

std::tcout << _T("\n matchBy tel is not empty.\n");
recordsetQuery rq;
rq.when(_T("tel"), _T("<>"), _T(""));
rs.matchBy(rq);
WScript.Echo('matchBy tel is not empty.');
var rq = new ActiveXObject('transactd.recordsetQuery');
rq.When('tel', '<>', '');
rs.MatchBy(rq);
puts('matchBy tel is not empty.')
rq = Transactd::RecordsetQuery.new()
rq.when('tel', '<>', '')
rs.matchBy(rq)
echo('matchBy tel is not empty.');
$rq = new RecordsetQuery();
$rq->when('tel', '<>', '');
$rs->matchBy($rq);
Console.WriteLine("\nmatchBy tel is not empty.");
RecordsetQuery rq = new RecordsetQuery();
rq.When("tel", "<>", "");
rs.MatchBy(rq);

Union

フィールドが順序まで含めてすべて同じ2つのレコードセットは、和集合を取ることができます。

ここまでのサンプルのrsにはgroup=3の行は含まれていません。rs3group=3の行を読み取って、rsに結合してみましょう。

std::tcout << _T("\n append(union)  group3 users.\n");
q.reset().select(_T("id"), _T("user_name"), _T("group"), _T("tel"))
  .where(_T("group"), _T("="), _T("3")).reject(0);
recordset rs3;
at.index(1).keyValue(3).read(rs3, q);
rs += rs3;
WScript.Echo('append(union)  group3 users.');
q.Reset().Select('id', 'user_name', 'group', 'tel').Where('group', '=', 3).Reject(0);
var rs3 = at.Index(1).KeyValue(3).Read(q);
rs.UnionRecordset(rs3);
puts('append(union)  group3 users.')
q.reset().select('id', 'user_name', 'group', 'tel').where('group', '=', 3).reject(0)
rs3 = at.index(1).keyValue(3).read(q)
rs.unionRecordset(rs3)
echo('append(union)  group3 users.');
$q->reset()->select('id', 'user_name', 'group', 'tel')->where('group', '=', 3)->reject(0);
$rs3 = $at->index(1)->keyValue(3)->read($q);
$rs->unionRecordset($rs3);
Console.WriteLine("\nappend(union)  group3 users.");
q.Reset().Select("id", "user_name", "group", "tel").Where("group", "=", 3).Reject(0);
Recordset rs3 = at.Index(1).KeyValue(3).Read(q);
rs.UnionRecordset(rs3);

Join

TransactdのJoinはすべてクライアント側で行われます。異なるデータべースやサーバーであってもJoinすることができます。

Joinには大きく分けて2種類の方法があります。

それぞれ、Table JoinRecordset Joinと呼びます。 ここでは、基本的なTable Joinについて説明します。

Recordset JoinについてはSDKの recordset::join を参照してください。

Table Join

Table Joinは、結合先のテーブルのインデックスを使って高速に処理できます。この方法を使用するには以下の制約があります。

テーブルとその関連が正しく設計されていれば、ほとんどのJoinはこの方法で行うことができます。

1対1のJoin

Joinを行う手順は以下の通りです。

  1. あらかじめ、結合前のrecordsetを取得しておきます。
  2. 結合するデータを持つactiveTableを生成します。
  3. 検索に使用するインデックスを番号で指定します。
  4. queryオブジェクトのselectを使って取得するフィールドを指定します。
  5. joinメソッドの引数に1のrecordsetと、その中の検索のキーとなるフィールド名を渡します。 このキーの値を使って検索したレコードのフィールドが結合されます。

ここまでのサンプルの結果セットrsgroupフィールドにはグループ番号が入っています。この番号から、グループの名前を結合してみましょう。

std::tcout << _T("\n Join group name.\n");
// open groups table
activeTable  at2(db, _T("groups"));
at2.index(0).alias(_T("name"), _T("group_name"));
// join
q.reset().select(_T("group_name"));
at2.join(rs, q, _T("group"));
WScript.Echo('Join group name.');
// open groups table
var at2 = new ActiveXObject('transactd.activeTable');
at2.SetDatabase(db, 'groups');
at2.Index(0).Alias('name', 'group_name');
// join
q.Reset().Select('group_name');
at2.Join(rs, q, 'group');
puts('Join group name.')
# open groups table
at2 = Transactd::ActiveTable.new(db, 'groups')
at2.index(0).alias('name', 'group_name')
# join
q.reset().select('group_name')
at2.join(rs, q, 'group')
echo('Join group name.');
// open groups table
$at2 = new ActiveTable($db, 'groups');
$at2->index(0)->alias('name', 'group_name');
// join
$q->reset()->select('group_name');
$at2->join($rs, $q, 'group');
Console.WriteLine("\nJoin group name.");
// open groups table
ActiveTable at2 = new ActiveTable();
at2.SetDatabase(db, "groups");
at2.Index(0).Alias("name", "group_name");
// join
q.Reset().Select("group_name");
at2.Join(rs, q, "group");

今回結合したgroupsテーブルのindex(0)はプライマリーキーでありユニークキーなので、1対1のJOINです。

1対多のJoin

今度は逆に、先にすべてのグループを読み取って、そこに属するユーザーをすべてJOINしてみます。グループ1つに対して所属するユーザーは複数になるので、1対多のJOINです。

std::tcout << _T("\n Has many join.\n");
// read all groups
rs.clear();
at2.index(0).keyValue(0)
  .alias(_T("id"), _T("group_id"))
  .alias(_T("name"), _T("group_name"))
  .read(rs, q.reset());
// join groups
at.alias(_T("id"), _T("user_id"))
  .alias(_T("name"), _T("user_name"));
q.reset().select(_T("user_id"), _T("user_name"));
at.index(1).join(rs, q, _T("group_id"));
WScript.Echo('Has many join.');
// read all groups
rs = at2.Index(0).KeyValue(0)
  .Alias('id', 'group_id')
  .Alias('name', 'group_name')
  .Read(q.Reset());
// join groups
at.Alias('id', 'user_id')
  .Alias('name', 'user_name');
q.Reset().Select('user_id', 'user_name');
at.Index(1).Join(rs, q, 'group_id');
puts('Has many join.')
# read all groups
rs = at2.index(0).keyValue(0)
  .alias('id', 'group_id')
  .alias('name', 'group_name')
  .read(q.reset())
# join groups
at.alias('id', 'user_id')
  .alias('name', 'user_name')
q.reset().select('user_id', 'user_name')
at.index(1).join(rs, q, 'group_id')
echo('Has many join.');
// read all groups
$rs = $at2->index(0)->keyValue(0)
  ->alias('id', 'group_id')
  ->alias('name', 'group_name')
  ->read($q->reset());
// join groups
$at->alias('id', 'user_id')
  ->alias('name', 'user_name');
$q->reset()->select('user_id', 'user_name');
$at->index(1)->join($rs, $q, 'group_id');
Console.WriteLine("\nHas many join.");
// read all groups
rs = at2.Index(0).KeyValue(0)
  .Alias("id", "group_id")
  .Alias("name", "group_name")
  .Read(q.Reset());
// join groups
at.Alias("id", "user_id")
  .Alias("name", "user_name");
q.Reset().Select("user_id", "user_name");
at.Index(1).Join(rs, q, "group_id");

結果はこのようになります。

+---------------------------------------------------+
|  group_id|group_name    |   user_id|user_name     |
+-------------------------+-------------------------+
|         1|Administrators|         1|user1         |
|         1|Administrators|         3|user3         |
|         2|DBOperators   |         2|user2         |
|         2|DBOperators   |         4|user4         |
|         2|DBOperators   |         9|user9         |
|         2|DBOperators   |        13|user13        |
|         3|Users         |         5|user5         |
|         3|Users         |         6|user6         |
|         3|Users         |         7|user7         |
|         3|Users         |         8|user8         |
|         3|Users         |        10|user10        |
|         3|Users         |        11|user11        |
|         3|Users         |        12|user12        |
|         3|Users         |        14|user14        |
|         3|Users         |        15|user15        |
+-------------------------+-------------------------+

OuterJoin

これまで見てきたJoinメソッドはINNER JOINです。レコードセットの各行において、結合する相手レコードが無い場合、その行は削除されます。

outerJoinメソッドはOUTER JOINです。相手レコードが無い場合でも元の行は削除されません。 相手レコードが見つからなかったレコードはisInvalidRecordtrueになり、フィールドはNULLで初期化されます。

エラー処理 (activeTableとrecordset クラス)

activeTableとrecordsetクラスのメソッドは失敗すると例外をスローします。適切な位置でエラーをキャッチするコードを必ず記述してください。

try
{
  activeTable at(db, "users");
  recordset rs;
  query q;
  at.read(rs, q);
  // ...
}
catch (bzs::rtl::exception& e)
{
  std::tcout << *(bzs::rtl::getMsg(e)) << std::endl;
}
try
{
  var at = new ActiveXObject('transactd.activeTable');
  at.SetDatabase(db, 'users');
  var rs = at.Read(q);
  // ...
}
catch(e)
{
  WScript.Echo("Error: " + e.name + " " + e.description);
}
begin
  at = Transactd::ActiveTable.new(db, 'users')
  at.read(q)
  # ...
rescue => e
  puts e.message
end
try
{
  $at = new ActiveTable(db, 'users');
  $rs = $at->read($q);
  // ...
}
catch (Exception e)
{
  echo($e->getMessage() .  PHP_EOL);
}
try
{
  ActiveTable at = new ActiveTable();
  Recordset rs = at.Read(q);
  // ...
}
catch (System.Runtime.InteropServices.COMException e)
{
  Console.WriteLine(e.Message);
}

データベースとテーブルの作成・削除

ここでは、Transactdを使用して、以下のSQL文で作成するのと同様なデータベースとテーブルを作成します。

CREATE DATABASE tdtutorial2;
CREATE TABLE tdtutorial2.users (id int(11) NOT NULL, name binary(33) NOT NULL, UNIQUE KEY key0 (id));

データベースの作成

database::createメソッドで、データベースとTransactd用のスキーマテーブルが作成されます。

_TCHAR errorMsg[1024] = { 0x00 };
database* db = database::create();
const _TCHAR* uri = _T("tdap://localhost/tdtutorial2?dbfile=transactd_schema");
db->create(uri);
// ...
/* close and release database object */
database::destroy(db);
var db = new ActiveXObject('transactd.database');
var URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema';
db.Create(URI);
db = Transactd::Database.new()
URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema'
db.create(URI)
const URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema';
$db = new Database();
$db->create(URI);
const string uri = "tdap://localhost/tdtutorial2?dbfile=transactd_schema";
Database db = new Database();
db.Create(uri);

スキーマテーブルには自由な名前を付けることができます。MySQLではファイル名の拡張子は無視されますが、Transactdが理解できるように拡張子をbdfとしてください。スキーマテーブルにtransactd_schemaを指定する場合は、拡張子は不要です。

テーブルの作成

テーブルの作成では、最初にテーブル定義を作成しスキーマに保存します。その後database::openTable()でそのテーブルを開こうとすると、実際のテーブルが生成されます。

テーブル定義を作成する手順は以下の通りです。

  1. databaseオブジェクトから、テーブルスキーマを管理するdbdefオブジェクトを取得します。
  2. テーブル定義オブジェクト(tabledef)を生成します。
  3. tabledefオブジェクトにテーブル名などの情報を設定します。
  4. dbdef::insertFieldメソッドで空のフィールド定義オブジェクト(fielddef)を生成します。
  5. fielddefオブジェクトにフィールド名や型などの情報を設定します。
  6. フィールドの数だけ4,5を繰り返します。
  7. dbdef::insertKeyメソッドで空のキー定義オブジェクト(keydef)を生成します。
  8. keydefオブジェクトにセグメントなどの情報を設定します。
  9. キーの数だけ7,8を繰り返します。
  10. dbdef::updateTableDefメソッドを使用して、スキーマテーブルに変更を反映します。

まずは手順1~3を行います。

dbdef::insertTableの結果としてテーブル定義を表すtabledefオブジェクトが返されるので、そこにテーブル名などの情報を設定します。

dbdef* def = db->dbDef();
tabledef td;

// table name and file name
td.setTableName(_T("users"));
td.setFileName(_T("users.dat"));

td.id = 1;
def->insertTable(&td);

dbdef::insertTableの結果としてテーブル定義を表すtabledefオブジェクトが返されるので、そこにテーブル名などの情報を設定します。

var dbdef = db.DbDef;
var td = dbdef.InsertTable(1);

// table name and file name
td.TableName = 'users';
td.FileName = 'users.dat';
td.Id = 1;

まずtabledefオブジェクトを生成しテーブル名などの情報を設定します。次に、dbdef::insertTableにそのテーブル定義を渡します。

dbdef = db.dbDef()
td = Transactd::Tabledef.new()

# table name and file name
td.setTableName('users')
td.setFileName('users.dat')

td.id = 1
dbdef.insertTable(td)

まずtabledefオブジェクトを生成しテーブル名などの情報を設定します。次に、dbdef::insertTableにそのテーブル定義を渡します。

$dbdef = $db->dbDef();
$td = new Tabledef();

// table name and file name
$td->setTableName('users');
$td->setFileName('users.dat');

$td->id = 1;
$dbdef->insertTable($td);

dbdef::insertTableの結果としてテーブル定義を表すtabledefオブジェクトが返されるので、そこにテーブル名などの情報を設定します。

DbDef dbdef = db.DbDef;
TableDef td = dbdef.InsertTable(1);

// table name and file name
td.TableName = "users";
td.FileName = "users.dat";
td.Id = 1;

tabledefの詳しい内容はSDKのtabledefクラスで参照できます。

手順4~6で、フィールドを追加していきます。

dbdef::insertFieldの結果としてフィールド定義を表すfielddefオブジェクトが返されるので、そこにフィールド名や型などの情報を設定します。

// [id] field
short fieldNum = 0;
fielddef* fd = def->insertField(td.id, fieldNum);
fd->setName(_T("id"));
fd->type = ft_integer;
fd->len = (ushort_td)4;

// [name] field
fd = def->insertField(td.id, ++fieldNum);
fd->setName(_T("name"));
fd->type = ft_zstring;
fd->len = 33;
// [id] field
var filedIndex = 0;
var fd = dbdef.InsertField(td.Id, filedIndex);
fd.Name = 'id';
fd.Type = ft_integer;
fd.Len = 4;

// [name] field
filedIndex = 1;
fd = dbdef.InsertField(td.Id, filedIndex);
fd.Name = 'name';
fd.Type = ft_zstring;
fd.Len = 33;
# [id] field
filedIndex = 0
fd = dbdef.insertField(td.id, filedIndex)
fd.setName('id')
fd.type = Transactd::Ft_integer
fd.len = 4

# [name] field
filedIndex = 1
fd = dbdef.insertField(td.id, filedIndex)
fd.setName('name')
fd.type = Transactd::Ft_zstring
fd.len = 33
// [id] field
$filedIndex = 0;
$fd = $dbdef->insertField($td->id, $filedIndex);
$fd->setName('id');
$fd->type = Transactd::ft_integer;
$fd->len = 4;

// [name] field
$filedIndex = 1;
$fd = $dbdef->insertField($td->id, $filedIndex);
$fd->setName('name');
$fd->type = Transactd::ft_zstring;
$fd->len = 33;
// [id] field
FieldDef fd = dbdef.InsertField(td.Id, 0);
fd.Name = "id";
fd.Type = eFieldType.ft_integer;
fd.Len = 4;

// [name] field
fd = dbdef.InsertField(td.Id, 1);
fd.Name = "name";
fd.Type = eFieldType.ft_zstring;
fd.Len = 33;

fielddefの詳しい内容はSDKのfielddefクラスで参照できます。 また、フィールドのタイプ名ft_integerなどがMySQLのどのような型に相当するかはSDKのデータタイプ対応表をご覧ください。

手順7~9で、キーを追加していきます。

dbdef::insertKeyの結果としてキー定義を表すkeydefオブジェクトが返されるので、そこにセグメントなどの情報を設定します。

設定が終わったら、手順10dbdef::updateTableDefメソッドを使用して、スキーマテーブルに変更を反映します。

// Insert key
uchar_td keyNum = 0;
keydef* kd = def->insertKey(td.id, keyNum);
kd->segments[0].fieldNum = 0;
kd->segments[0].flags.bit8 = true; //extended type 
kd->segments[0].flags.bit1 = true; //updateable
kd->segmentCount = 1;

// set id as primary key
def->tableDefs(td.id)->primaryKeyNum = keyNum;

// update schema table
def->updateTableDef(td.id);
// add key
var keyNum = 0;
var key = dbdef.InsertKey(td.Id, keyNum);
var seg1 = key.Segments(0);
seg1.FieldNum = 0;
seg1.Flags.Bits(key_extend) = true;
seg1.Flags.Bits(key_changeable) = true;
key.SegmentCount = 1;

// set id as primary key
tableDef.PrimaryKeyNum = keyNum;

// update schema table
dbdef.UpDateTableDef(td.Id);
# add key
keyNum = 0
key = dbdef.insertKey(td.id, keyNum)
key.segment(0).fieldNum = 0
key.segment(0).flags.bit8 = 1 # key_extend
key.segment(0).flags.bit1 = 1 # key_changeable
key.segmentCount = 1

# set id as primary key
tabledef.primaryKeyNum = keyNum

# update schema table
dbdef.updateTableDef(td.id)
// add key
$keyNum = 0;
$key = $dbdef->insertKey($td->id, $keyNum);
$key->segment(0)->fieldNum = 0;
$key->segment(0)->flags->bit8 = 1; // key_extend
$key->segment(0)->flags->bit1 = 1; // key_changeable
$key->segmentCount = 1;

// set id as primary key
$td->primaryKeyNum = $keyNum;

// update schema table
$dbdef->updateTableDef($td->id);
// Insert key
byte keyNum = 0;
KeyDef kd = dbdef.InsertKey(td.Id, keyNum);
KeySegment seg = kd[0];
seg.FieldNum = 0;
seg.Flags[key_extend] = true;      // extended type
seg.Flags[key_changeable] = true;  // updateable
kd.SegmentCount = 1;

// set id as primary key
td.PrimaryKeyNum = keyNum;

// update schema table
dbdef.UpDateTableDef(td.Id);

keydefの詳しい内容はSDKのkeydefクラスで参照できます。

この時点ではスキーマテーブルが更新されているだけで、テーブルは作成されていません。このテーブルを最初にopenする際に自動でテーブルが作成されます。

テーブルとデータベースの削除

database::dropTableメソッドを使用すると、テーブルを削除できます。ただし、テーブルを使用中のユーザーがいると失敗します。

dropTableはテーブルを削除しますが、スキーマ内のテーブル定義は削除しません。テーブル定義を削除する場合はdbdef::deleteTable(short tableIndex)を使用します。

database::dropメソッドを使用すると、データベースを削除できます。データベースを削除すると、その中に含まれたスキーマテーブルも同時に削除されます。

database* db = database::create();
static const _TCHAR* uri = _T("tdap://localhost/tdtutorial2?dbfile=transactd_schema");
db->open(uri);
// drop table
db->dropTable(_T("users"));
// drop database
db->drop();
/* close and release database object */
database::destroy(db);
var db = new ActiveXObject('transactd.database');
var URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema';
db.Open(URI);
// drop table
db.DropTable('users');
// drop database (then all tables in the database will be dropped.)
db.Drop();
db = Transactd::Database.new()
URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema'
db.open(URI)
# drop table
db.dropTable('users')
# drop database (then all tables in the database will be dropped.)
db.drop()
$db = new Database();
const URI = 'tdap://localhost/tdtutorial2?dbfile=transactd_schema';
$db->open(URI);
// drop table
$db->dropTable('users');
// drop database (then all tables in the database will be dropped.)
$db->drop();
const string uri = "tdap://localhost/tdtutorial2?dbfile=transactd_schema";
Database db = new Database();
db.Open(uri);
// drop table
db.DropTable("users");
// drop database (then all tables in the database will be dropped.)
db.Drop();

サンプルコードダウンロード

より詳しいリファレンス