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++
- ActiveX(COM):C#やJScript他、Windowsの各種言語から利用可能
- Ruby
- PHP
言語ごとにインストール方法を説明します。
C++用クライアントは、使用するコンパイラがLinuxのgccもしくはWindowsのVisual Studio 2010であれば、ビルド済みバイナリをインストールすることができます。 Linuxはx86-64Bit 、Windowsはx86-32Bit/64Bitの両方があります。LinuxはLSBに基づいた Genericなバイナリです。
それ以外のコンパイラの場合はソースコードからクライアントライブラリをビルドします。
詳しいインストール方法はインストールガイドを参照してください。
ビルド済みのCOMコンポーネントをインストールすることができます。
COMコンポーネントはWindows用のx86-32Bit/64Bitの両方が用意されています。
詳しいインストール方法はインストールガイドを参照してください。
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
データベースにはusers
とgroups
の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.php
をinclude_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
項目 | 説明 |
---|---|
tdap | Transactdプロトコルを表します。 |
username | MySQLユーザー名を指定します。(ユーザーとホストの組み合わせ認証時のみ。デフォルトでは必要ありません。) |
host | ホスト名またはIPアドレスを指定します。 |
database | データベース名を指定します。 |
schema table | dbfile= でスキーマテーブルを指定します。
transactd_schema と指定すると、スキーマテーブルを自動生成します。Transactd 3.0以降では、スキーマテーブルの指定を省略し、スキーマ情報を自動取得することもできるようになりました。 |
password | pwd= でパスワードを指定します。(ユーザーとホストの組み合わせ認証時のみ。デフォルトでは必要ありません。) |
スキーマテーブルとは
スキーマテーブルは、データベース内にある全テーブルの構成情報を格納した、Transactdプラグイン用のメタ情報テーブルです。
スキーマテーブルは既存のデータベースから自動生成することができます。open
時にスキーマテーブル名をtransactd_schema
にすると、データベースのテーブル情報を読み取って自動生成されます。
Transactd 3.0以降では、スキーマ情報をテーブルとして保存せず、毎回自動生成することもできるようになりました。
テーブルの構造をSQLで変更した場合
SQLのCREATE TABLE
やALTER 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
最初の読み取り
テーブルからデータを読み取る手順は以下の通りです。
- 検索に使用するテーブルのインデックスを番号で指定します。
- そのインデックスのフィールドに、キーとなる値をセットします。
seek
系のメソッドを実行します。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');
- 型に応じて用意された
V64
Vlng
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");
- 型に応じて用意された
V64
Vlng
Vbin
Text
などの引数付きプロパティを使用して、値をセットもしくは取得します。
これで、指定したレコードを検索しデータを読み取ります。この読み取ったレコード(行)がカレント行です。 キー値で指定したレコードが無い場合はカレント行は不定状態になります。
ここで使用したメソッドなどを補足します。
- フィールドにアクセスする関数は、名前ではなく番号(ゼロorign)で指定することも可能です。番号でのアクセスは名前でのアクセスより高速に動作します。
seek
はキー値が一致したレコードを取得します。seekGreater / seekLessThan
は、キー値が一致するか、それより後(もしくは前)の最初のレコードを取得します。OR_EQUAL
パラメータにfalse
を指定すると、指定したキー値と一致するレコードは含まず、それより後(もしくは前)の最初のレコードを取得します。seekFirst / seekLast
は、インデックス順で最初(もしくは最後)のレコードを取得します。
メソッド | 該当する演算子と取得するレコード | |
---|---|---|
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レコードずつ移動することができます。
seekPrev
、seekNext
は前回取得した次のレコードを取得します。 これらのメソッドを使う場合はその前のオペレーションでレコードの取得に成功している(サーバー側のカレントレコードが存在している)必要があります。- オペレーションの結果は
stat
で取得します。成功すると0
が返ります。これ以上ない場合はエラーコードSTATUS_EOF
が返ります。
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
テーブルにレコードを追加する手順は以下の通りです。
- 最初に
clearBuffer()
を呼び出してすべてのフィールドの値をゼロまたは空文字""に初期化します。 - 次に
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
既存のレコードを更新する手順は以下の通りです。
- 検索に使用するテーブルのインデックスを番号で指定します。
- そのインデックスのフィールドに、キーとなる値をセットします。
seek
系のメソッドを実行して更新するレコードを読み取ります。- 必要なフィールドの値を変更して
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
はカレント行を指定した値で上書きします。カレント行が存在しないまま行うと、stat
にSTATUS_NO_CURRENT
が返ります。
また、読み取りと更新の間にそのレコードが別のクライアントによって変更または削除されていた場合は、コンフリクトを検出しstat
にSTATUS_CHANGE_CONFLICT
が返ります。
update
の引数にchangeinkey
を指定すると、事前にカレントを移動することなくキー値でカレントを確立し更新することができます。(ユニークキーを指定し、キー値は更新しない場合)
Delete
既存のレコードを削除する手順は以下の通りです。
- 検索に使用するテーブルのインデックスを番号で指定します。
- そのインデックスのフィールドに、キーとなる値をセットします。
seek
系のメソッドを実行して削除するレコードを確定します。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
はカレント行を削除します。カレント行が存在しないまま行うと、stat
にSTATUS_NO_CURRENT
が返ります。
また、読み取りと削除の間にそのレコードが別のクライアントによって変更または削除されていた場合は、コンフリクトを検出しstat
にSTATUS_CHANGE_CONFLICT
が返ります。
delete
の引数にinkey=true
を指定すると、事前にカレントを移動することなくキー値でカレントを確立し削除することができます。(ユニークキーを指定した場合)
Transaction
トランザクションを使って、複数レコードの更新をアトミックに行ってみましょう。
id=2
とid=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
オブジェクトの単位で行われます。
トランザクション内での読み取りは、そのレコードが必ずロックされます。そのため、update
やdelete
でコンフリクトが起こることはありません。
レコードロックに関するより詳しい情報は、SDKの「InnoDBのロックとその制御」またはブログ記事「TransactdでInnoDBロックを自在に操る」を参考にしてください。
エラー処理(database, table クラス)
これまで紹介したメソッドの処理結果はそれぞれのクラスのstat
で取得します。
stat() == 0
がtrueであれば成功、そうでなければ失敗を示します。
database
とtable
クラスのオペレーションは例外をスローしません。
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
クラスと併せて使用することで、SELECT
やWHERE
といった表現を使用でき、SQLに近い感覚でデータの読み取りができます。
読み取ったデータの結果セットであるrecordset
は、OrderBy
やMutchBy
、GroupBy
といった演算を行うことができます。また、結果セットに対して異なるテーブルをJoin
することもできます。
activeTableでのread
activeTable
を使用してデータを読み取る手順は以下の通りです。
activeTable
を生成します。- 検索に使用するテーブルのインデックスを番号で指定します。
- そのインデックスのフィールドに、キーとなる値をセットします。
query
オブジェクトを使って取得対象と検索終了条件を指定します。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
なので、index
はgroup
に付けられたものを使用します。キー番号は作成順に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
は結果セットを並べ替えるメソッドです。
recordset
をid
フィールドを基準に並び替えてみましょう。
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回の読み取りで明細と集計結果の両方を取得できます。
取得済みのrs
をrs2
にコピーしてみましょう。
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
でグルーピングを行う手順は以下の通りです。
groupQuery
を生成します。- グルーピングに使用する
KeyField
を指定します。 - 計算関数と結果フィールドの名前を指定します。
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|
+----------+----------+
計算関数クラスには他にもsum
、avg
、max
、min
などがあります。
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
のようなフィルタリングが行えます。
recordsetQuery
を生成します。- フィルタリングの条件を指定します。
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
の行は含まれていません。rs3
にgroup=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種類の方法があります。
- 結果セットの結合値をキーに、テーブルからデータを読んで結合する方法。
recordset - table 結合
- 2つの結果セットを結合する方法。
recordset - recordset 結合
それぞれ、Table Join
Recordset Join
と呼びます。
ここでは、基本的なTable Join
について説明します。
Recordset Join
についてはSDKの
recordset::join
を参照してください。
Table Join
Table Joinは、結合先のテーブルのインデックスを使って高速に処理できます。この方法を使用するには以下の制約があります。
- テーブルの結合フィールドは、インデックスフィールドでなければならない。(複数セグメントキーの場合、下位フィールドは対象外にできる)
- 検索のインデックスは前項のインデックスを使用する。
- 結合条件は
=
のみ使用可能。
テーブルとその関連が正しく設計されていれば、ほとんどのJoinはこの方法で行うことができます。
1対1のJoin
Join
を行う手順は以下の通りです。
- あらかじめ、結合前の
recordset
を取得しておきます。 - 結合するデータを持つ
activeTable
を生成します。 - 検索に使用するインデックスを番号で指定します。
query
オブジェクトのselect
を使って取得するフィールドを指定します。join
メソッドの引数に1のrecordset
と、その中の検索のキーとなるフィールド名を渡します。 このキーの値を使って検索したレコードのフィールドが結合されます。
ここまでのサンプルの結果セットrs
のgroup
フィールドにはグループ番号が入っています。この番号から、グループの名前を結合してみましょう。
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
です。相手レコードが無い場合でも元の行は削除されません。
相手レコードが見つからなかったレコードはisInvalidRecord
がtrue
になり、フィールドは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()
でそのテーブルを開こうとすると、実際のテーブルが生成されます。
テーブル定義を作成する手順は以下の通りです。
database
オブジェクトから、テーブルスキーマを管理するdbdef
オブジェクトを取得します。- テーブル定義オブジェクト(
tabledef
)を生成します。 tabledef
オブジェクトにテーブル名などの情報を設定します。dbdef::insertField
メソッドで空のフィールド定義オブジェクト(fielddef
)を生成します。fielddef
オブジェクトにフィールド名や型などの情報を設定します。- フィールドの数だけ4,5を繰り返します。
dbdef::insertKey
メソッドで空のキー定義オブジェクト(keydef
)を生成します。keydef
オブジェクトにセグメントなどの情報を設定します。- キーの数だけ7,8を繰り返します。
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();