はじめに
Embarcadero のブログ記事「基礎から学べる FireDAC データアクセス再入門 (番外編)」で FireDAC によるコネクションプーリングの手法が示されていました。これを Interbase Express (IBX) でやってみようというのが本記事の趣旨となります。
Interbase Express (IBX) とは
Interbase Express (IBX) は Embarcadero 社の InterBase へアクセスする手段を提供する一連のコンポーネント群です。サードパーティ製扱いになっていたと思います。InterBase 専用のコンポーネントですが、自己責任で Firebird SQL に接続する事も可能です。
See also:
- Interbase (Embarcadero)
- InterBase Express 入門 (DocWiki)
- Firebird SQL (firebirdsql.org)
- 【Delphi】IBX (InterBase Express) で Firebird を使う (Qiita)
IBX コンポーネントとコネクションプーリング
Delphi におけるデータベースのマルチスレッドプログラミングは、コンポーネントの差異に関わらず、
- スレッド毎にコネクションを確立する。
これに尽きます。つまり、単一接続しか許容しないローカルデータベース等ではデータベースのマルチスレッドプログラミングが難しいという事になります。
接続の確立はそれなりに高コストなので、データベース系のプログラムをマルチスレッド化したからといって必ずしも高速化するとは限りません。それでもコネクションプーリングの手法を採用すれば、パフォーマンスの向上を見込める場合があります。
TIBConnectionBroker
IBX でコネクションプーリングを利用するには TIBConnectionBroker を使います 1。しかしながらこのコンポーネントはドキュメントが一切存在しません。それではあんまりなので、FireDAC のサンプルを模倣して説明したいと思います。
今回の記事を書くために Delphi 11.0 Alexandria を使っていますが、Community Edition (10.4 Sydney) を使う事もできます。
FireDAC のサンプル同様、DB サーバーとして Interbase 2020 Developer Edition を使用します。事前にインストールを済ませておいてください。
See also:
TIBConnectionBroker の使い方
(1) VCLフォームアプリケーションのプロジェクト作成
Delphi IDE のメニューから [ファイル | 新規作成 | Windows VCLフォームアプリケーション]
を選択します。
(2) プロジェクトを保存する
メニューの [ファイル | すべて保存]
を選択し、全てのファイルを保存してください。プロジェクトは任意のフォルダに保存することができます。
(3) フォーム上に IBX のコンポーネントを配置する
ツールパレットの [Interbase]
カテゴリから
- TIBConnectionBroker
- TIBDatabase
- TIBTransaction
- TIBQuery
(4) フォーム上にUIコンポーネントを配置する
ツールパレットの [Standard]
カテゴリから
- Label
を 4 つ
- TCheckBox
を 1 つ
- TButton
を 1 つ
(5) 配置したコントロールのプロパティを変更する
Label1
プロパティ | 値 |
---|---|
Caption | 実行回数: |
Label2
プロパティ | 値 |
---|---|
Caption | 実行時間: |
Label3
プロパティ | 値 |
---|---|
Caption | — |
Label4
プロパティ | 値 |
---|---|
Caption | — |
CheckBox1
プロパティ | 値 |
---|---|
Caption | コネクションプーリング |
Button1
プロパティ | 値 |
---|---|
Caption | 実行 |
各プロパティの値を変更後の画面イメージは、下図の通りです。
面倒なのでフォームのコードを貼っておきます。
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 214
ClientWidth = 500
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -15
Font.Name = 'Segoe UI'
Font.Style = []
PixelsPerInch = 96
TextHeight = 20
object Label1: TLabel
Left = 32
Top = 32
Width = 67
Height = 20
Caption = #23455#34892#22238#25968':'
end
object Label2: TLabel
Left = 32
Top = 64
Width = 67
Height = 20
Caption = #23455#34892#26178#38291':'
end
object Label3: TLabel
Left = 128
Top = 32
Width = 15
Height = 20
Caption = #8212
end
object Label4: TLabel
Left = 128
Top = 64
Width = 15
Height = 20
Caption = #8212
end
object CheckBox1: TCheckBox
Left = 48
Top = 110
Width = 157
Height = 27
Caption = #12467#12493#12463#12471#12519#12531#12503#12540#12522#12531#12464
TabOrder = 0
end
object Button1: TButton
Left = 60
Top = 156
Width = 125
Height = 29
Caption = #23455#34892
TabOrder = 1
end
object IBConnectionBroker1: TIBConnectionBroker
TransactionIdleTimer = 0
Left = 284
Top = 64
end
object IBQuery1: TIBQuery
BufferChunks = 1000
CachedUpdates = False
ParamCheck = True
PrecommittedReads = False
Left = 404
Top = 64
end
object IBDatabase1: TIBDatabase
ServerType = 'IBServer'
Left = 284
Top = 128
end
object IBTransaction1: TIBTransaction
Left = 404
Top = 128
end
end
IBX のコンポーネントを配置していますが、プロパティの変更は必要ありません。 これらのコンポーネントを配置している主な目的は、プロジェクトをビルドしたときに IBX 関連のユニットをソースコードの uses 句に自動的に追加させるためです。
(6) データベースへアクセスするためのスレッドクラスを定義
TThread クラスから派生する、IBX オブジェクトヘアクセスするためのスレッドクラスを定義します。ここでは、TDBThread
クラスという名前で定義します。Unit1.pas を開いて、interface 句に以下のコードを追加してください。
type
TDBThread = class(TThread)
private
FForm: TForm1;
public
constructor Create(AForm: TForm1);
procedure Execute; override;
end;
(7) Form1 クラスにメソッドと変数を定義
Form1 クラスに実行の開始時間や実行回数の値を保持するメンバー変数と、これらを表示するためにメソッドを定義します。Unit1.pas を開いて、Form1 クラスに以下のコードを追加してください。
type
TForm1 = class(TForm)
..
..
private
{ Private 宣言 }
FCount: Integer;
FStartTime: LongWord;
public
{ Public 宣言 }
procedure Executed;
end;
(8) Form1 クラスの Executed メソッドを実装
手順 (7) で定義した Form1 の Executed メソッドの実装コードを追加します。Unit1.pas を開いて、以下のコードを追加してください。
procedure TForm1.Executed;
begin
Inc(FCount);
// 10 回ごとに Label3 に回数を表示する
if (FCount mod 10) = 0 then
Label3.Caption := IntToStr(FCount);
// 実行回数が 500 に達したら、実行時間を表示する
if FCount = 500 then
begin
// 表示される実行時間は、ms 単位
Label4.Caption := FloatToStr((GetTickCount - FStartTime) / 1000.0);
Button1.Enabled := True;
end;
(9) スレッドクラスのコンストラクタを実装
スレッドクラスのコンストラクタのコードを実装します。Unit1.pas を開いて、以下のコードを追加してください。
constructor TDBThread.Create(AForm: TForm1);
begin
FForm := AForm;
FreeOnTerminate := True;
inherited Create(False);
end;
FreeOnTerminate は、スレッド終了時にスレッドオブジェクトを自動的に破棄するかどうかを決定します。FreeOnTerminate が False のときは自動的に破棄されません。
(10) Form1 クラスの OnCreate イベントハンドラを実装
オブジェクトンスペクタの [イベント] タブで OnCreate イベントをダブルクリックしてイベントハンドラを作成します。
Unit1.pas を開いて、以下のコードを追加してください。
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
CheckBox1: TCheckBox;
Button1: TButton;
IBConnectionBroker1: TIBConnectionBroker;
IBQuery1: TIBQuery;
IBDatabase1: TIBDatabase;
IBTransaction1: TIBTransaction;
procedure FormCreate(Sender: TObject); // <- 追加される
private
{ Private 宣言 }
FCount: Integer;
FStartTime: LongWord;
public
{ Public 宣言 }
procedure Executed;
end;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
with IBConnectionBroker1 do
begin
MaxConnections := 20;
DatabaseName := '127.0.0.1:C:\Embarcadero\Studio\22.0\InterBase2020\examples\database\employee.gdb';
Params.Clear;
Params.Values['user_name'] := 'SYSDBA'; // User Name
Params.Values['password' ] := 'masterkey'; // Password
Params.Values['lc_ctype' ] := 'none'; // CharSet
Init;
end;
end;
employee.gdb の場所は InterBase 2020 をインストールした場所によって異なります。
(11) スレッドクラスの Execute メソッドを実装
スレッドクラスの Execute メソッドのコードを実装します。本スレッドは、IBX のオブジェクトへアクセスすることが目的のため、TIBDatabase によるデータベースへの接続と解放をこのスレッド内で行っています。Unit1.pas を開いて、以下のコードを追加してください。
procedure TDBThread.Execute;
var
oConn: TIBDatabase;
oQuery: TIBQuery;
oTran: TIBTransaction;
i: Integer;
begin
if FForm.CheckBox1.Checked then
oConn := FForm.IBConnectionBroker1.GetConnection
else
begin
oConn := TIBDatabase.Create(nil);
oConn.DatabaseName := FForm.IBConnectionBroker1.DatabaseName;
oConn.Params.Text := FForm.IBConnectionBroker1.Params.Text;
oConn.LoginPrompt := False;
oConn.Connected := True;
end;
oTran := TIBTransaction.Create(nil);
oTran.DefaultDatabase := oConn;
oQuery := TIBQuery.Create(nil);
try
oQuery.Database := oConn;
oQuery.Transaction := oTran;
for i := 1 to 50 do
begin
oQuery.SQL.Text := 'select * from Employee';
oQuery.Open;
oConn.Close; // ※ ???
Synchronize(FForm.Executed); // VCL の描画スレッドへ同期
end;
finally
oQuery.Free;
oTran.Free;
if FForm.CheckBox1.Checked then
FForm.IBConnectionBroker1.ReleaseConnection(oConn)
else
oConn.Free;
end;
end;
ソースコードの ※ の部分は oQuery.Close; じゃないかと思います。コネクションに時間が掛かるという話をしているのに、わざわざ切断して再接続しているため、コネクションプーリングの意味があまりないような気がします。実際 oQuery.Close; に変更した方が高速に動作します。
(12) 実行ボタンのコードを実装
設計画面で Button1 をダブルクリックすると、Button1 の OnClick のイベントハンドラが生成されます。そのイベントハンドラ内に、スレッドを実行する処理などを実装します。
var
i: Integer;
begin
Button1.Enabled := False;
FStartTime := GetTickCount;
FCount := 0;
Label3.Caption := '---';
Label4.Caption := '---';
for i := 1 to 10 do
begin
// スレッドの生成
TDBThread.Create(Self);
end;
(13) プロジェクトを保存する
メニューの [ファイル | すべて保存]
を選択し、全てのファイルを保存してください。
(14) アプリケーションを実行する
ツールバー(上図)の [実行]
ボタン、または、キーボードの〔F9〕
ボタンを押します。
(15) [実行]ボタンを押す
[実行]
ボタンを押すと、クエリーが 500 回実行され 2、実行時間 (ms) が表示されます。
(16) コネクションプーリングにチェックを入れて、[実行] ボタンを押す
今度は、コネクションプーリングにチェックを入れて、[実行] ボタンを押してください。同様にクエリーが 500 回実行され 2、実行時間 (ms) が表示されます。
いかがでしょうか?
実行結果を比較すると、コネクションプーリングを有効にすると同じ実行回数でも実行時間 (ms) が、より短いことが実感できるかと思います。
今回のサンプルブログラムはスレッドの実行回数も少なく、ローカル接続している InterBase へのアクセスということもあって、接続確立時のオーバーヘッドもそこまで多くはありません。そのため、コネクションプーリング有無の差をそれほど感じられないかもしれませんが、実際のシステムでは接続の試行回数はもっと多いでしょうし、データベースサーバーへはリモート接続となるため、その差は顕著になると思われます。
IBX 以外のコンポーネントとコネクションプーリング
ざっと他のコンポーネントの状況についても紹介しておきます。基本的に接続コンポーネントはスレッド毎に生成する必要があります。
・BDE
BDE の場合には TSession をスレッド毎に作成する必要があります。TSession をフォームやデータモジュールに貼り付けて管理するより、Sessions.OpenSession()
で動的に作成して使うのが簡単だと思います。
with Sessions.OpenSession(セッション名) do
begin
Active := False;
NetFileDir := 共有フォルダ名;
PrivateDir := プライベートフォルダ名;
Active := True;
end;
コネクションプーリングに相当するのは MTS Pooling です。
この値をプログラムからオンオフするにはレジストリ HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Borland\Database Engine\Settings\SYSTEM\INIT
3 にある MTS POOLING
の値を変更します。
ただ、この機構は正しく動作しているようには思えません。サンプルフォルダ (Object Pascal\Database\IBX\MtsPool
) に MTS Pool という、先述の BDE のレジストリと IBX を使ったプロジェクトもあるのですが、こちらも正しく動作しているとは思えません。
Delphi 4 には、これの元となった BDE ベースの MTS Pool デモがありますが、こちらは "当時は" 正しく動作していたのかもしれません (NT4.0 の頃)。
BDE を使っていないのに IBX だけ使ったアプリケーションで BDE 用のレジストリのスイッチを一つ変えただけで速度が変化するとか有り得ない気がしますが (最近の Delphi では BDE コンポーネントが別になっているので暗黙的に組み込まれる事もない)、もし本当に高速化するというのならどういった仕組みで高速化するのでしょうね?4
See also:
- データベース セッションの管理 (DocWiki)
- 複数セッションの管理 (DocWiki)
- Microsoft Transaction Server (Wikipedia)
- IBX.MtsPool Sample (DocWiki)
- IBX.MtsPool Sample (GitHub)
・dbGo (ADO Express)
ADO の場合には、接続文字列の最後に次の値を追加してコネクションプーリング機能のオンオフを切り替えられます。
コネクションプーリング | 接続文字列 |
---|---|
有効 | OLE DB Services = -1 |
無効 | OLE DB Services = -2 |
接続文字列全体で判断して、プールされた接続が再利用されるかどうか決定されるようですね。
Microsoft SQL Server とかはともかく、Microsoft Access は「そもそもマルチスレッドで使って大丈夫なのかな?」と思わなくもありません。
See also:
・dbExpress (DBX)
TSQLConnection のパラメータを追加してやるとよいようです。
パラメータ | 値 |
---|---|
DelegateConnection | DBXPoolConnection |
DBXPoolConnection.DriverName | DBXPool |
DBXPoolConnection.MaxConnections | 20 |
次のコードのようになります。DBXC は TSQLConnection 型の変数です。
DBXC.Params.values['DelegateConnection'] := 'DBXPoolConnection';
DBXC.Params.values['DBXPoolConnection.DriverName'] := 'DBXPool';
DBXC.Params.values['DBXPoolConnection.MaxConnections'] := '20';
DBX は基本的に Enterprise 以上の SKU でしか使えない 5 ため未検証です。
See also:
・ZeosLib
TZConnection の Protocol プロパティに指定されているプロトコル名を pooled.
で修飾する事でコネクションプーリングが行われるようになります。
See also:
おわりに
IBX によるコネクションプーリングのやり方でした。解ってしまえばなんてことはないですね。
DB コンポーネントをいくつも動的作成するのがメンドイ?そんな場合はデータモジュールに DB コンポーネントを貼って、データモジュールごと動的作成すればいいと思うよ。
See also:
-
TIBConnectionBroker は Delphi 7 で実装されていますが、Delphi 6 でも IBX アップデータをインストールすれば使えると思います。 ↩
-
32bit Windows なら
HKEY_LOCAL_MACHINE\SOFTWARE\Borland\Database Engine\Settings\SYSTEM\INIT
となります。 ↩ -
実は Interbase クライアントがこのレジストリキーを見て挙動を変えてるとか? ↩
-
Professional 版ではローカル接続しかできないという前時代的な制約があります。 ↩