BizStationブログ

ビズステーション株式会社の公式ブログです。

Transactd 2.0 その1 SQLライクなクエリーと結果セット

いよいよTransactd 2.0をリリースします。Transactd 2.0で新しくなったのは主に、

です。順番に詳しく紹介してゆきます。今回は「SQLライクなクエリーと結果セット」です。

Transactdの従来のAPIはどんな処理でも書けますが、SQLに慣れた方にとってはあまりフレンドリーではありませんでした。特に、JOINを含むレコードの読み取り・検索は、SQLと大きく異なっています。

そこで、SQLライクなAPIと結果を扱うrecordsetクラスが追加されました。
recordset連想配列によく似た結果セットで、rs[1]["name"]のように行・列に添え字でアクセスできます。内部実装はC++であり、PHPでもRubyでも高速にSQLライクな処理ができます。

紹介のためのサンプルコードはPHPで書いてみます。メソッドの多くは、メソッドチェーンができるようにthisを返します。

OrderBy

まずはOrderByrecordset rs"group"というフィールドで昇順にソートします。

$rs->orderBy("group");

逆順にするにはreverseを使います。

$rs->reverse();

複数のフィールドで昇順、降順が混在するorderByは、sortFieldsクラスにフィールド名とソート方向をセットして渡します。
"group"で昇順、"tel"で降順にするならば、以下のようになります。

$sort = new sortFields();
$sort->add('group', true).add('tel', false);
$rs->orderBy($sort);

GroupBy

次に、GroupBygroupQueryオブジェクトに情報をセットしてrecordsetgroupByメソッドに渡します。

$gq = new groupQuery();
...
$rs->groupBy(gq);

groupQueryオブジェクトには複数の計算関数を追加できます。計算関数には対象のフィールドと結果フィールド名を指定します。
例は"group"でグルーピングして各グループに属するレコード数をカウントし"group_count"という列に結果を格納します。

$gq = new groupQuery();
$gq->keyField("group");           //<---- グルーピングキーフィールドの指定
$func = new count("group_count"); //<---- 関数と結果列名の指定
$gq->addFunction($func);
$rs->groupBy($gq);

計算関数にはcountの他にもsum avg min max があります。

レコードごとに計算の対象とするかどうか指定したいときは、計算関数のオブジェクトにWhenフィルタを設定できます。
"tel" != ""の時だけカウントしたいときは

$func->when("tel", "<>" ,"");

でフィルタリングできます。さらに、

$func->when("tel", "<>" ,"")->and_("tel" , "<>", "090*");

のようにand_()or_()条件をメソッドチェーンでつないでいくこともできます。

また、グルーピングのキーは複数のフィールドとすることもできます。

$gq->keyField("group", "name");

MatchBy

結果セットを得たあとでフィルタリングできます。マッチしない行はレコードセットから削除されます。

$rq = new recordsetQuery();
$rq->when("count", ">", "1")->and_("tel", "<>", "");
$rs->matchBy(rq);

途中結果をコピー

SQLでは完全な結果を得るのみで途中結果を得ることはできません。
たとえば、売上の明細とその金額の合計を得たい場合、明細のクエリーと合計を計算するクエリーの2つが必要です。(または合計を自分で計算します。)

Transactdでは、処理をクライアント側で順番に書くので、途中の結果セットをコピーしておくだけで両方の結果を得られ効率的です。

...
$rs2 = $rs->clone();
...

$rs2->groupBy(gq);
// rsは明細
// rs2は合計

レコードの読み取り

新しく読取専用クラスactiveTableクラスが追加されました。
activeTableには主に、read, join, outerJoin の3つのメソッドしかありません。

まずは単純なreadです。データベースとテーブル名からactiveTableを生成します。
次にクエリーを作成してactiveTablereadに渡すと、結果セット(recordset)が返ってきます。
クエリーは、select, where, and_, or_メソッドSQLライクに記述できます。

まず、activeTableを生成します。

$at = new activeTable($db, "user");

検索には必ずindexを番号で指定します。

$at->index(1);

検索を開始する先頭レコードの値を指定します。この値はindexで指定したキーフィールドの値です。

$at->index(1)->keyValue("1");

次にqueryオブジェクトを使ってSQLライクに検索条件を指定します。

$q = new query();
$q->select("id", "名前", "group")->where("group", "<=", "2")

フィールドに別名を付けてみましょう。

$at->alias("名前", "name");

最後に読み取ります。

$rs = $at->read($q);

全部まとめて書くと (クラスの名前空間 BizStation\Transactdは省略)

$db = new database();
$uri = "tdap://localhost/test?dbfile=transactd_schema";
$db->open($uri, TYPE_BDF, OPEN_NORMAL);
$at = new activeTable($db, "user");
$at->alias("名前", "name");
$q = new query();
$q->select("id", "name", "group")->where("group", "<=", "2")
$rs = $at->index(1)->keyValue("1")->read($q);

すべてのフィールドを読み取りたいときは、selectメソッドでフィールドを選択しないようにします。そうするとデフォルトですべてのフィールドが返ります。

$q = new query();
$q->where("group", "<=", "2")
$rs = $at->index(1)->keyValue("1")->read($q);

Join

さあいよいよ、核心のJoinです。
TransactdのJoinは、サーバーサイドでの結合ではなく、クライアントサイドで結合します。結合は、事前に取得したrecordsetとこれから読み取るテーブルのフィールドで行います。

1対1のJoin

例として$rsは上記の結果セットとします。結果セットのgroupフィールドにはグループ番号が入っています。この番号からgroupsテーブルのnameフィールドをJoinしてみましょう。

$at2 = new activeTable($db, "groups");
$q->select('name');                      //<--- name 列だけを読み取る
$at2->index(0)->join($rs, $q, "group");  //<--- 結果セットのgroup列をキーとしてJoin

これで、userテーブルから得た結果セットにgroupsテーブルのnameフィールドをJoinしました。
内部では、recordsetgroupフィールドの値をgroupsindexで指定されたキーフィールドにセットして、順にレコードを取得しています。この場合、index(0)groupsのプライマリーユニークキーなので、1対1のJoinです。

キーがマルチセグメントキーの場合は、それぞれのセグメントに対応するrecordsetのフィールドを指定します。
例えば、groupsテーブルのprimaryキーがtype + idなら

$at2 = new activeTable($db, "groups");
$q->select('name');                      
$at2->index(0)->join($rs, $q, "type", "group"); 

のようにjoinメソッドで2つのフィールドを指定します。

1対多のJoin

recordset内の1行に対して複数の相手レコードがある場合は1対多のJoinです。
この場合、指定するインデックスは、重複可能なキーか、マルチセグメントのユニークキーで下位セグメントが未指定、のどちらかになります。

呼び出しは、1対1の場合とほとんど同じです。
例えば、まず、1~100までのグループを読み取って、そこに属するユーザーをすべてJoinしてみましょう。
最初に1~100までのグループを読み取ります。

$at = new activeTable($db, "groups");
$at2->alias("id", "group_id");
$q = new query();
$q->where("group_id", "<=", "100")
$rs = $at->index(0)->keyValue("1")->read($q);

次にそこに属するユーザーのidnameをJoinしてみます。

$at2 = new activeTable($db, "user");
$at2->alias("名前", "name");
$q->reset()->select("id", "name");
$at2->index(1)->join($rs, $q, "group_id"); //<--- index=1はgroupフィールドの重複可キー

これで、読み取ることができました。$q->reset()queryオブジェクトの状態をリセットして、オブジェクトを使いまわすために呼び出しています。

OuterJoin

先ほどまでのJoinはInnerJoinで、相手レコードが見つからなかった場合は、recordset内の該当行は削除されます。
それに対して、outerJoinメソッドを使うと、相手レコードが見つからなかった場合でも元の行はそのまま残ります。追加されたフィールドは数値0、文字列では""で初期化されます。

Joinの最適化オプション

TransactdのJoinには1つだけ最適化オプションがあります。

Joinメソッドは、テーブルにアクセスする前にrecordset内の各行のJoinのキー値を調べ、同じものがないか走査し、同値であればそれを省いて読み取りを行い、読み取られるレコード数を削減する処理を行っています。
ところが、各行のJoinのキー値がユニークであることがわかっている場合、この処理はムダです。そこでこの処理を省くためのオプションが用意されています。

recordesetの各行のJoinキー値がユニークであるとわかっている場合は、queryオブジェクトのoptimizejoinHasOneOrHasManyを指定します。

$q->optimize(joinHasOneOrHasMany);

Union

2つのrecordsetオブジェクトを和結合できます。$rs$rs2を結合します。

$rs->unionRecordset($rs2);

ただし、2つのレコードセットのフィールドは順序も含めてすべて同じでなければなりません。もし異なる場合は例外が投げられます。

recordsetのそのほかの主な機能

列情報

recordsetfieldDefsメソッドで、fielddefオブジェクトのコレクションが返ります。
例は、すべてのフィールド名を表示しています。

$fds = $rs->fieldDefs();
foreach($fd in $fds)
{
   echo $fd->name().PHP_EOL;
}
Top

recordsetの先頭N行を返します。

$rsTop10 = new recordset(); 
$rs->top($rsTop10, 10)

もし、$rsの行数がN行に満たない場合は、すべての行がコピーされます。

FirstとLast

先頭行および最終行を返します。

$firstRecord = $rs->first();
$lastRecord = $rs->last();
removeField

N番目(0Orign)の列を削除します。

$rs->removeField(3);
appendField

列を末尾に追加します。

$rs->appendField("mobile phone", ft_myvarchar/*type*/, 30/*length*/);

まとめ

ご覧いただいた通り、activeTablerecordsetの組み合わせを用いると、SQLライクな表現でSQLとほぼ同様な結果を簡単に得ることができます。処理の順序や使用するインデックスの指定など、プログラマの思い通りにとてもフレキシブルに処理できます。

なお、SQLのような四則演算や文字列操作関数は、各言語で可能な処理ですので用意されていません。SQLは独立した言語なのでそのような関数がありますが、TransactdはAPIですので、それぞれの言語に用意されたものを使用できます。

次回は、今回ご紹介したSQLライクなクエリーのソースコードををVisualな操作で簡単に生成してくれる、queryBuilderをご紹介したいと思います。queryBuilderは、WindowsMac OS X 10.9 でC++/PHP/Ruby/JScriptのソースを生成できます。