Transactd tutorial

Introduction

Target

This tutorial is written for the beginner who learn Transactd for the first time. It shows the basic concepts of Transactd and how to use API, with sample codes.

The knowledge about SQL is not required. But you should install MySQL/MariaDB and use its command line clients.

Index

Set up plugin and clients

Transactd is composed of two modules. "Transactd plugin" works as the server plugin for MySQL/MariaDB. "Transactd clients" is used in user programs. This section guides how to set up these modules.

Install plugin and clients

MySQL/MariaDB and Transactd plugin

"Transactd plugin" works as the server plugin for MySQL/MariaDB. Add plugin module to your installed MySQL/MariaDB server.

To install MySQL / MariaDB, please refer to the documentation of each products.

To install Transactd plugin, please refer to Install guide.

After installation, please do Show status of Transactd Plugin to check installation.

Transactd clients

The modules for following programming languages are provided now:

Pre-built C++ client binaries are available for Linux 64bit GCC and Windows Visual Studio 32bit/64bit. The binaries for Linux is generic binary which based on LSB.

For other compilers, build client binaries from source code.

Please refer to Install guide.

Pre-built COM component binaries are available.

COM component for x86-32bit/64bit are provided.

Please refer to Install guide.

Transactd client for Ruby is available on RubyGems.org.

Please refer to Install guide.

For Linux, build PHP client binaries from source code.

Pre-built binaries for PHP are available on Windows.

Please refer to Install guide.

Pre-built COM component binaries are available.

COM component for x86-32bit/64bit are provided.

Please refer to Install guide.

Sample database and table

Execute following SQL (with command line client or other tool) to create user database 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 database has users and groups table.

Prepare for each programming language

In this tutorial, sample codes works actually. How to use Transactd in program code is different between each programming language.

Configure following directory settings to include Transactd header file and link library files.

Windows

  • Add source directory in Transactd source code package or SDK to include path.
    (ex: C:\transactd-client-win64_with_sdk-2.4.3\source)
  • Add BOOST path to include path.
    (ex: C:\boost\boost_1_58_0)
  • Add lib directory in SDK or build directory to library path to link with tdclcpp_xxx_xx_xxx.lib.
    (ex: C:\transactd-client-win64_with_sdk-2.4.3\lib)
  • Add tdclcpp_xxx_xx_xxx.lib to project to link it, or, add TRDCL_AUTOLINK to preprocessor of project property in Visual Studio or Embarcadero C++ Builder.

Linux

  • Add source directory in Transactd source code package or SDK to include path.
    (ex: ~/transactd-client-linux-x86_64_with_sdk-2.4.3/source)
  • Add にsource/linux directory to include path too.
    (ex: ~/transactd-client-linux-x86_64_with_sdk-2.4.3/source/linux)
  • Add BOOST path to include path.
    (ex: ~/boost/boost_1_58_0)
  • Add the directory where libtdclcpp_64m.so.2.4 is installed to library path. Configure Makefile to link tdclcpp_64m. Makefile Example

JScript program does not need compiling.

Ruby program does not need compiling.

PHP program does not need compiling.

Select Visual Studio menu - [Project] - [Add Reference...] and open [Reference Manager]. Enable transactd in [COM] list.

After adding transactd to Reference, you can see Transactd's constant values, functions and classes in [Object Browser].

include, require

#include header files in SDK and standard library header file which is used in sample.

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

Define using namespace to omit the name space.

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

JScript program does not need require or include. Create instance with new ActiveXObject.

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

Transactd client which installed from RubyGems.org can be used with require, as same as other gem packages.

require 'transactd'

You should require wrapper file transactd.php. It contains Transactd's constant values, functions, classes and useful interfaces like ArrayAccess.

  • Put transactd.php in PHP include_path, then require('transactd.php');.
  • require('path/to/transactd.php');.

The namespace where classes are is BizStation\Transactd. In this tutorial, use to omit long namespace.

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;

Define using to omit the name space.

using transactd;

Operation on databases and tables

Open and close database

Let's open database first.

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();

COM releases database object memory automatically. The database will be closed automatically when memory releasing, but it is preferred to call Close explicitly.

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()

Ruby's GC releases database object memory automatically. The database will be closed automatically when memory releasing, but it is preferred to call close explicitly.

$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();

PHP's GC releases database object memory automatically. The database will be closed automatically when memory releasing, but it is preferred to call close explicitly.

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();

COM releases database object memory automatically. The database will be closed automatically when memory releasing, but it is preferred to call close explicitly.

To open database, create database object first. Next, call open method with URI parameter. After some operations, close database with close method.

URI format

There is format of URI which was passed to open method:

tdap://username@localhost/tdtutorial?dbfile=transactd_schema&pwd=password
‾‾‾‾   ‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ‾‾‾‾‾‾‾‾‾‾‾‾
scheme username    host    database        schema table        password
ElementDescription
tdapIt means "use Transactd protocol".
username User name on MySQL. (It is not required in default. Only with "Authentication by combination of users and hosts".)
hostHost name or IP address of server.
databaseDatabase name.
schema tableSpecify schema table name with dbfile=. transactd_schema means "generate schema table automatically".
(Transactd 3.0 or later) Schema table is optional. If it is not specified, get schema information automatically.
password Password with pwd=. (It is not required in default. Only with "Authentication by combination of users and hosts".)

What is "schema table"?

"schema table" is a meta information table for Transactd plugin. It contains schema information of all tables in database.

The schema table can be generated automatically from an existing database. Specify transactd_schema as schema table name on open, Transactd will read table information and will generate schema table automatically.

On Transactd 3.0 or later, schema table is optional. If you want, no schema table will be saved, and read table schema information on each database open.

After modifing table schema with SQL

If you modify table schema with CREATE TABLE or ALTER TABLE SQL, there is the difference between the information stored in schema table and actual schema information.

In such case, drop schema table once. Then open database with schema table name transactd_schema. The schema table will be generated automatically with the latest information.

Specify close(true /* withDropDefaultSchema */) to drop transactd_schema table on closing database.

Connection

database object manages TCP or named-pipe connections. If there is no connection, new connection will be created on open. So you do not have to care about it.

Prease reffer to multi thread application page (Japanese) too.

Open and close table

Next, open a table.

// ...
// 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();

Call openTable to open a table, and call close to close the table. Please be careful that close the database after close the table.

stat of database object show the result status code of the operation. Success code is 0.

Read

First reading

How to read data from table:

  1. Set the index which will be used in searching.
  2. Set key value to the indexed field(s).
  3. Call seek methods.
  4. Check the result status code with stat. Success code is 0.
// 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"));
  • Set values with setFV. First argument is field name, next is value. There are some overloaded functions according to the type of value, such as _TCHAR* int64 int short double float.
  • Get values with getFVxxx. There are some overloaded functions according to the type of value, such as 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');
  • There are some "property with parameter" according to the type of value, such as V64Vlng Vbin Text. Set or get values with them.
# 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
  • Set values with setFV. First argument is field name, next is value.
  • Get values with getFVxxx. There are some overloaded functions according to the type of value, such as 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);
  • Set values with setFV. First argument is field name, next is value.
  • Get values with getFVxxx. There are some overloaded functions according to the type of value, such as 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");
  • There are some "property with parameter" according to the type of value, such as V64Vlng Vbin Text. Set or get values with them.

Seek a record which match with specified key value in above code. The found record (row) is "current row". If record was not found, current row is undefined.

Add more information about functions in above code.

MethodOperator and match record
seek()=Record which match with specified key value
seekGreater(OR_EQUAL = true)>= First record which match with specified key value or greater than
seekGreater(OR_EQUAL = false)> First record which is greater than specified key value
seekLessThan(OR_EQUAL = true)<= First record which match with specified key value or less than
seekLessThan(OR_EQUAL = false)< First record which is less than specified key value
seekFirst()First record with index order
seekLast()Last record with index order

Server cursor

Before next step, introduce "Server cursor".

The current row in Transactd's table class is managed by server side. For example, nstable::seekNext() method moves current row to next row in server, and send the data to client. This kind of cursor is called Server cursor.

Moving row

Now you get current row, so you can move current row with seekPrev or seekNext.

Read all records with seekFirst and seekNext

Let's read all records on users table.

// 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();
}

Read records which match "id >= 2 and id <= 5" with seekGreater

Next, read records whose id is in range 2 to 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();
}

In the case of using seekNext, the order of records is order of index. In this case, records are order by id. So checking exit condition in loop is very easy.

Insert

How to insert record to table:

  1. Call clearBuffer() to initialize all field values.
  2. Set field values to table object, and call 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

How to update existing record:

  1. Set the index which will be used in searching.
  2. Set key value to the indexed field(s).
  3. Call seek methods and find target record.
  4. Change field values and callupdate.

For example, search the record which have id=2, and update name value to 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 method overwrites current row. If you call it without getting current row, stat returns STATUS_NO_CURRENT error code.

If the target record has been updated or deleted by other client between your reading and updating, conflict will be detected, stat returns STATUS_CHANGE_CONFLICT error code.

If changeinkey is specified to update, you can omit finding target record before. Seek current row with key value automatically. (In the case of using unique key without updating key value.)

Delete

How to delete a record:

  1. Set the index which will be used in searching.
  2. Set key value to the indexed field(s).
  3. Call seek methods and find target record.
  4. Call delete.

For example, delete the record which have 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 method deletes current row. If you call it without getting current row, stat returns STATUS_NO_CURRENT error code.

If the target record has been updated or deleted by other client between your reading and deleting, conflict will be detected, stat returns STATUS_CHANGE_CONFLICT error code.

If inkey=true is specified to delete, you can omit finding target record before. Seek current row with key value automatically. (In the case of using unique key.)

Transaction

Use transaction to update multi records atomically.

Change user names on id=2 and id=3 records with updateUserName function. Update two records atomically.

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.");
}

Start transactions with beginTrn and commit it with endTrn. Call abortTrn to rollback.

Transaction runs on database object unit.

Reading in transaction gets lock of target record(s). So conflict will not be occurred on update or delete. More detail about record lock, please refer to InnoDB lock and control it (Japanese) or Control InnoDB lock with Transactd (Japanese).

Error handling (database, table class)

The result status code of the methods which was introduced above is got by stat on each class. If stat() == 0 is success. Others are failed. Operations in database and table classes do not throw any exceptions.

The description of error codes which is returned by stat are listed in Error code table (Japanese). Or, call statMsg to get error message.

_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());
}

You can download sample code with error handling.

Read like SQL

Error handling code is snipped from samples in this section to clearify essence of the code. Files in sample download contain error handling code.

activeTable

activeTable is class that access to data with SQL-like method.

table class accesses to server with one record unit. activeTable class can read multiple records in once, and it is more faster.

query class provides sql-like phrases such as SELECT or WHERE.

recordset is result data set class. It has operations such as OrderBy, MutchBy and GroupBy. In addition, you can Join another table to recordset.

Read with activeTable

How to read data with activeTable:

  1. Create activeTable object.
  2. Set the index which will be used in searching.
  3. Set key value to the indexed field(s).
  4. Set search conditions with query object.
  5. read data.

Now, read user records which has 2 or lesser group value from users table. Following code show step 1 to 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);

There are overloaded keyValue same as table::setFV. For example, _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();

Set index which contains group field as index, because search condition is group. Key number is desided in order of creation, start from 0. In this case, group index is 1.

The value of keyValue is start position. Normally set lower value. In this case, set 0.

You can set alias to fields with alias method.

In addition, methods which set index or field value or alias returns self atciveTable object to use method chain.

Next, set conditions to query as step 4:

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

There are overloaded where same as table::setFV. For example, _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);

List up fields which will be read with select. select can receive 11 fields in once. If you want to more fields, call select several times.

Set searching condition with where. Pass field name, operator, and value. and_ or or_ can continue multiple conditions.

reject(1) means that finish searching if a field which do not match condition is found. In this case, the condition contains key field, so searching will be finished immediately if unmatch record (group=3) was found. Use reject efficiently to reduce search time. Set reject(0xFFFF) to do not finish searching even if unmatch records were found.

query's methods which set values returns self query object, as same as activeTable.

Last, step 5, read data. The result is recordset object. The code which contain all steps is:

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 values like this:

+---------------------------------------------------------------+
|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 contains rows and fields. It is similar to two-dimensional array. You can access data with row number and field name, like rs[1]["name"].

Let's see rows and fields in recordset with loop.

// 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;
  }
}

Read functions for each type are provided. For example, 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);
  }
}

Get field definitions (fielddefs) with fieldDefs. You can access data with [] on this collection, too.

recordset has SQL-like methods, such as OrderBy, GroupBy, Union, Join. See more details in next section.

OrderBy

OrderBy sorts recordset. Let's sort recordset with order of id.

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

Sort with more complex order, such as mix of ascending and descending order.

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);

Add order settings (field name and ASC/DESC) with sortFields object. Pass it to OrderBy method.

Clone

If you want to get both of result details and aggregated result, there are two way by SQL. (1) Execute two queries, or, (2) Get detail result and aggregate it on program code.

Transactd's recordset is copyable. If you got recordset, copy it and aggregate copied recordset. In this way, data reading is only once.

Copy rs to rs2:

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

recordset has magic method __clone(). Use clone statement.

Recordset rs2 = rs.Clone();

GroupBy

How to grouping records with GroupBy:

  1. Create groupQuery object.
  2. Set grouping KeyField.
  3. Set calculation function and result field name.
  4. Group with groupBy.

Group by group field value and count records with countfunction, store result to 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);

The result is:

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

There are more calculation functions such as sum, avg, max, min.

You can filter records with when. For example, exclude records which have blank 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);

The number of user on group 1 was decreased.

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

MatchBy

How to filter result set with MatchBy:

  1. Create recordsetQuery object.
  2. Set filtering condition.
  3. Filter with MatchBy.

Remove records which have blank tel from result set.

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

Two recordsets which have same field (in same order) can be united.

rs does not have records which group=3. Read records which have group=3 to rs3, and unite them.

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

All Transactd JOIN is done on the client side. You can JOIN records between different databases and servers.

JOIN has two kinds of methods.

They are called Table Join and Recordset Join. I will explain basics Table Join here.

About Recordset Join, see recordset::join in SDK document.

Table Join

Table Join can be processed at high speed, by using the index of the joined table. The following restrictions apply to using this:

Most Join can do this way if the tables and their relationships are designed correctly.

1 on 1 Join

How to Join:

  1. Get recordset.
  2. Create activeTable which have table you want to join.
  3. Set the index which will be used in searching.
  4. select fields with query object.
  5. Pass recordset (step 1) and field name which will be used as searching key to join.

rs has group id in group field. Let's join group name with this id.

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");

In this case, index(0) on groups is primary key (it means it is unique key), then this is 1 on 1 join.

"HasMany" Join

Read all group first, and join users to them. It is "HasMany" join because a group has many users.

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");

The result is:

+---------------------------------------------------+
|  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

Above Join are INNER JOIN. If there is no record to join, the record will be removed from result recordset.

outerJoin does not remove such records. If there is no record to join, the record will be marked as isInvalidRecord=true, and the field will be initialized with NULL.

Error handling (activeTable and recordset class)

Methods of activeTable and recordset throws exceptions if operation failed. Write exception catching code.

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);
}

Create and drop databases and tables

Create database and table which is same as that created by following SQL:

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

Create database

database::create creates database and schema table for 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);

You can set any name to schema table. Set extension as bdf. If you use transactd_schema as schema table, extension is not required.

Create table

First, create table definition and save it to schema table. After that, the table will be created automatically when you open table with database::openTable().

How to create table definition:

  1. Get dbdef object from database object.
  2. Create table definition object tabledef.
  3. Set information such as table name to tabledef object.
  4. Create blank field definition object fielddef with dbdef::insertField.
  5. Set information such as field name and field type to fielddef object.
  6. Repeat step 4 and step 5 for all fields.
  7. Create blank key definition object keydef with dbdef::insertKey.
  8. Set information such as key segments to keydef object.
  9. Repeat step 7 and step 8 for all keys.
  10. Update schema table with dbdef::updateTableDef.

Step 1 to step 3:

dbdef::insertTable returns tabledef object. Set table information to it.

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 returns tabledef object. Set table information to it.

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

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

Create tabledef object and set information to it. Then pass it to 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)

Create tabledef object and set information to it. Then pass it to 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 returns tabledef object. Set table information to it.

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

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

Please refer to tabledef class reference (Japanese) for more details.

Add fields according to step 4 to step 6.

dbdef::insertField returns fielddef object. Set field information to it.

// [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;

Please refer to fielddef class reference (Japanese) for more details. What MySQL data type is compatible with Transactd data type, such as ft_integer, is listed in Data type table (Japanese).

Add keys according to step 7 to step 9.

dbdef::insertKey returns keydef object. Set key information to it.

After that, step 10, call dbdef::updateTableDef to update schema table.

// 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);

Please refer to keydef class reference (Japanese) for more details.

Table schema has been updated, but the table has not been created yet at this point. The table will be created automatically when you open it for the first time.

Drop database and table

database::dropTable drops table. It will fail if there is some user who use this table.

dropTable drops table, but it does not delete table definition in schema table. Use dbdef::deleteTable(short tableIndex) to delete table definition.

database::drop drops database. It also drops schema table which is contined in the database.

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();

Download sample code

More information