【Delphi】マルチスレッドアプリケーションでの IBX の利用

# はじめに
Embarcadero のブログ記事「[基礎から学べる FireDAC データアクセス再入門 (番外編)](https://blogs.embarcadero.com/ja/firedac-tutorial-07-ja/)」で FireDAC によるコネクションプーリングの手法が示されていました。これを **Interbase Express (IBX)** でやってみようというのが本記事の趣旨となります。

# Interbase Express (IBX) とは
Interbase Express (IBX) は Embarcadero 社の **InterBase** へアクセスする手段を提供する一連のコンポーネント群です。サードパーティ製扱いになっていたと思います。InterBase 専用のコンポーネントですが、自己責任で **Firebird SQL** に接続する事も可能です。

**See also:**

 - [Interbase (Embarcadero)](https://www.embarcadero.com/jp/products/interbase)
 - [InterBase Express 入門 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/InterBase_Express_%E5%85%A5%E9%96%80)
 - [Firebird SQL (firebirdsql.org)](https://www.firebirdsql.org/)
 - [【Delphi】IBX (InterBase Express) で Firebird を使う (Qiita)](https://qiita.com/ht_deko/items/a51df59c34a6c859296b)

# IBX コンポーネントとコネクションプーリング

Delphi におけるデータベースのマルチスレッドプログラミングは、コンポーネントの差異に関わらず、

 - スレッド毎にコネクションを確立する。

これに尽きます。つまり、単一接続しか許容しないローカルデータベース等ではデータベースのマルチスレッドプログラミングが難しいという事になります。

接続の確立はそれなりに高コストなので、データベース系のプログラムをマルチスレッド化したからといって必ずしも高速化するとは限りません。それでもコネクションプーリングの手法を採用すれば、パフォーマンスの向上を見込める場合があります。

## TIBConnectionBroker
IBX でコネクションプーリングを利用するには [TIBConnectionBroker](https://docwiki.embarcadero.com/Libraries/ja/IBX.IBConnectionBroker.TIBConnectionBroker) を使います [^2]。しかしながらこのコンポーネントはドキュメントが一切存在しません。それではあんまりなので、FireDAC のサンプルを模倣して説明したいと思います。

今回の記事を書くために **Delphi 11.0 Alexandria** を使っていますが、**Community Edition (10.4 Sydney)** を使う事もできます。

FireDAC のサンプル同様、DB サーバーとして **Interbase 2020 Developer Edition** を使用します。事前にインストールを済ませておいてください。

**See also:**

 - [Delphi Community Edition (Embarcadero)](https://www.embarcadero.com/jp/products/delphi/starter)
 - [ID: 17742, IBConnectionBroker example (Code Central)](https://cc.embarcadero.com/Item/17742)


## TIBConnectionBroker の使い方

### (1) VCLフォームアプリケーションのプロジェクト作成

Delphi IDE のメニューから `[ファイル | 新規作成 | Windows VCLフォームアプリケーション]` を選択します。

### (2) プロジェクトを保存する

メニューの `[ファイル | すべて保存]` を選択し、全てのファイルを保存してください。プロジェクトは任意のフォルダに保存することができます。

### (3) フォーム上に IBX のコンポーネントを配置する

ツールパレットの `[Interbase]` カテゴリから

 - TIBConnectionBroker
 - TIBDatabase
 - TIBTransaction
 - TIBQuery

### (4) フォーム上にUIコンポーネントを配置する

ツールパレットの `[Standard]` カテゴリから

 - Label

を 4 つ

 - TCheckBox

を 1 つ

 - TButton

を 1 つ

フォーム上の任意の位置へ配置します（下図の配置例を参照）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/93f1ee8a-ae25-9cdf-246c-40ff4a3b73cb.png)

### (5) 配置したコントロールのプロパティを変更する

**Label1**

| プロパティ | 値 |
|:---|:---|
|Caption|実行回数:|

**Label2**

| プロパティ | 値 |
|:---|:---|
|Caption|実行時間:|

**Label3**

| プロパティ | 値 |
|:---|:---|
|Caption|—|

**Label4**

| プロパティ | 値 |
|:---|:---|
|Caption|—|

**CheckBox1**

| プロパティ | 値 |
|:---|:---|
|Caption|コネクションプーリング|

**Button1**

| プロパティ | 値 |
|:---|:---|
|Caption|実行|

各プロパティの値を変更後の画面イメージは、下図の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/eec9ccfd-42ec-75b9-00a2-b64afff57b79.png)
面倒なのでフォームのコードを貼っておきます。

```pascal:Unit1.dfm
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
```

:::note info
IBX のコンポーネントを配置していますが、プロパティの変更は必要ありません。 これらのコンポーネントを配置している主な目的は、プロジェクトをビルドしたときに IBX 関連のユニットをソースコードの uses 句に自動的に追加させるためです。
:::

### (6) データベースへアクセスするためのスレッドクラスを定義

TThread クラスから派生する、IBX オブジェクトヘアクセスするためのスレッドクラスを定義します。ここでは、`TDBThread` クラスという名前で定義します。Unit1.pas を開いて、**interface** 句に以下のコードを追加してください。

```pascal:Unit1.pas
type
  TDBThread = class(TThread)
  private
    FForm: TForm1;
  public
    constructor Create(AForm: TForm1);
    procedure Execute; override;
  end;
```

### (7) Form1 クラスにメソッドと変数を定義

Form1 クラスに実行の開始時間や実行回数の値を保持するメンバー変数と、これらを表示するためにメソッドを定義します。Unit1.pas を開いて、Form1 クラスに以下のコードを追加してください。

```pascal:Unit1.pas
type
  TForm1 = class(TForm)
..
..
 private
    { Private 宣言 }
    FCount: Integer;
    FStartTime: LongWord;
  public
    { Public 宣言 }
    procedure Executed;
  end;
```

### (8) Form1 クラスの Executed メソッドを実装

手順 (7) で定義した Form1 の Executed メソッドの実装コードを追加します。Unit1.pas を開いて、以下のコードを追加してください。

```pascal: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 を開いて、以下のコードを追加してください。

```pascal:Unit1.pas
constructor TDBThread.Create(AForm: TForm1);
begin
  FForm := AForm;
  FreeOnTerminate := True;
  inherited Create(False);
end;
```

:::note info
FreeOnTerminate は、スレッド終了時にスレッドオブジェクトを自動的に破棄するかどうかを決定します。FreeOnTerminate が False のときは自動的に破棄されません。
:::


### (10) Form1 クラスの OnCreate イベントハンドラを実装

オブジェクトンスペクタの [イベント] タブで OnCreate イベントをダブルクリックしてイベントハンドラを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/01295a90-0182-8647-4158-beb9a892a287.png)
Unit1.pas を開いて、以下のコードを追加してください。

```pascal: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;
```

:::note warn
employee.gdb の場所は InterBase 2020 をインストールした場所によって異なります。
:::

### (11) スレッドクラスの Execute メソッドを実装

スレッドクラスの Execute メソッドのコードを実装します。本スレッドは、IBX のオブジェクトへアクセスすることが目的のため、TIBDatabase によるデータベースへの接続と解放をこのスレッド内で行っています。Unit1.pas を開いて、以下のコードを追加してください。

```pascal: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;
```

:::note warn
ソースコードの ※ の部分は oQuery.Close; じゃないかと思います。コネクションに時間が掛かるという話をしているのに、わざわざ切断して再接続しているため、コネクションプーリングの意味があまりないような気がします。実際 oQuery.Close; に変更した方が高速に動作します。
:::

### (12) 実行ボタンのコードを実装

設計画面で Button1 をダブルクリックすると、Button1 の OnClick のイベントハンドラが生成されます。そのイベントハンドラ内に、スレッドを実行する処理などを実装します。

```pascal:Unit1.pas
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) アプリケーションを実行する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/98d239d2-255a-10a6-16b5-33fe66a81ce1.png)
ツールバー（上図）の `[実行]` ボタン、または、キーボードの`〔F9〕`ボタンを押します。

### (15) [実行]ボタンを押す

`[実行]` ボタンを押すと、クエリーが 500 回実行され [^3]、実行時間 (ms) が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/5cc160a0-8396-c3fb-7f2c-00dab3713d05.png)

### (16) コネクションプーリングにチェックを入れて、[実行] ボタンを押す

今度は、コネクションプーリングにチェックを入れて、[実行] ボタンを押してください。同様にクエリーが 500 回実行され [^3]、実行時間 (ms) が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/4ab04486-7fb5-f187-bdd7-5b12a028c056.png)
いかがでしょうか?

実行結果を比較すると、コネクションプーリングを有効にすると同じ実行回数でも実行時間 (ms) が、より短いことが実感できるかと思います。

今回のサンプルブログラムはスレッドの実行回数も少なく、ローカル接続している InterBase へのアクセスということもあって、接続確立時のオーバーヘッドもそこまで多くはありません。そのため、コネクションプーリング有無の差をそれほど感じられないかもしれませんが、実際のシステムでは接続の試行回数はもっと多いでしょうし、データベースサーバーへはリモート接続となるため、その差は顕著になると思われます。

## IBX 以外のコンポーネントとコネクションプーリング
ざっと他のコンポーネントの状況についても紹介しておきます。基本的に接続コンポーネントはスレッド毎に生成する必要があります。

### ・BDE

BDE の場合には TSession をスレッド毎に作成する必要があります。TSession をフォームやデータモジュールに貼り付けて管理するより、`Sessions.OpenSession()` で動的に作成して使うのが簡単だと思います。

```pascal
  with Sessions.OpenSession(セッション名) do
  begin
    Active := False;
    NetFileDir := 共有フォルダ名;
    PrivateDir := プライベートフォルダ名;
    Active := True;
  end;
```

コネクションプーリングに相当するのは **MTS Pooling** です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/272d854b-0ec2-65fb-75f0-030eaac24665.png)
この値をプログラムからオンオフするにはレジストリ `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Borland\Database Engine\Settings\SYSTEM\INIT` [^1] にある `MTS POOLING` の値を変更します。

ただ、この機構は正しく動作しているようには思えません。サンプルフォルダ (`Object Pascal\Database\IBX\MtsPool`) に **MTS Pool** という、先述の BDE のレジストリと IBX を使ったプロジェクトもあるのですが、こちらも正しく動作しているとは思えません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/83536427-e70c-c8d4-3491-0eb0a243338d.png)
Delphi 4 には、これの元となった BDE ベースの MTS Pool デモがありますが、こちらは "当時は" 正しく動作していたのかもしれません (NT4.0 の頃)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/e35cff4a-c157-0d70-997c-a06069977eb0.png)
BDE を使っていないのに IBX だけ使ったアプリケーションで BDE 用のレジストリのスイッチを一つ変えただけで速度が変化するとか有り得ない気がしますが (最近の Delphi では BDE コンポーネントが別になっているので暗黙的に組み込まれる事もない)、もし本当に高速化するというのならどういった仕組みで高速化するのでしょうね？[^4]

**See also:**

 - [データベース セッションの管理 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/Sydney/ja/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9_%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E7%AE%A1%E7%90%86%EF%BC%9A%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9)
 - [複数セッションの管理 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E8%A4%87%E6%95%B0%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E7%AE%A1%E7%90%86)
 - [Microsoft Transaction Server (Wikipedia)](https://ja.wikipedia.org/wiki/Microsoft_Transaction_Server)
 - [IBX.MtsPool Sample (DocWiki)](https://docwiki.embarcadero.com/CodeExamples/en/IBX.MtsPool_Sample)
 - [IBX.MtsPool Sample (GitHub)](https://github.com/Embarcadero/RADStudio10.4Demos/tree/master/Object%20Pascal/Database/IBX/MtsPool)

### ・dbGo (ADO Express)
ADO の場合には、接続文字列の最後に次の値を追加してコネクションプーリング機能のオンオフを切り替えられます。

| コネクションプーリング | 接続文字列 |
|:---|:---|
| 有効 | OLE DB Services = -1 |
| 無効 | OLE DB Services = -2 |

接続文字列全体で判断して、プールされた接続が再利用されるかどうか決定されるようですね。

Microsoft SQL Server とかはともかく、Microsoft Access は「そもそもマルチスレッドで使って大丈夫なのかな？」と思わなくもありません。

**See also:**

 - [dbGo の概要 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/DbGo_%E3%81%AE%E6%A6%82%E8%A6%81)
 - [Connection Pooling (delphipower.xyz)](https://www.delphipower.xyz/guide_8/connection_pooling.html)

### ・dbExpress (DBX)

TSQLConnection のパラメータを追加してやるとよいようです。

| パラメータ | 値 |
|:---|:---|
| DelegateConnection | DBXPoolConnection |
| DBXPoolConnection.DriverName | DBXPool |
| DBXPoolConnection.MaxConnections | 20 |


次のコードのようになります。DBXC は TSQLConnection 型の変数です。

```pascal
  DBXC.Params.values['DelegateConnection'] := 'DBXPoolConnection';
  DBXC.Params.values['DBXPoolConnection.DriverName'] := 'DBXPool';
  DBXC.Params.values['DBXPoolConnection.MaxConnections'] := '20';
```

DBX は基本的に Enterprise 以上の SKU でしか使えない [^5] ため未検証です。

**See also:**

 - [dbExpress の機能の概要 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/DbExpress_4_%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AE%E6%A6%82%E8%A6%81)
 - [How do I set up DBX connection pooling in code? (StackOverflow)](https://stackoverflow.com/questions/10218813/how-do-i-set-up-dbx-connection-pooling-in-code)

### ・ZeosLib
TZConnection の Protocol プロパティに指定されているプロトコル名を `pooled.` で修飾する事でコネクションプーリングが行われるようになります。

**See also:**

 - [ZeosLib (SourceForge)](https://sourceforge.net/projects/zeoslib/)
 - [Connection Pooling in Zeos (SourceForge)](https://sourceforge.net/p/zeoslib/wiki/Connection%20Pooling/)


# おわりに
IBX によるコネクションプーリングのやり方でした。解ってしまえばなんてことはないですね。

DB コンポーネントをいくつも動的作成するのがメンドイ？そんな場合はデータモジュールに DB コンポーネントを貼って、データモジュールごと動的作成すればいいと思うよ。

**See also:**

 - [Delphi コンカレント (並行処理) プログラミング (Qiita)](https://qiita.com/ht_deko/items/e8c1ff3a4c74e4c2a4f3)

[^1]: 32bit Windows なら `HKEY_LOCAL_MACHINE\SOFTWARE\Borland\Database Engine\Settings\SYSTEM\INIT` となります。
[^2]: TIBConnectionBroker は Delphi 7 で実装されていますが、Delphi 6 でも [IBX アップデータ](https://cc.embarcadero.com/Item/24266)をインストールすれば使えると思います。
[^3]: スレッドは 10 回しか実行されない気が。
[^4]: 実は Interbase クライアントがこのレジストリキーを見て挙動を変えてるとか？
[^5]: Professional 版ではローカル接続しかできないという前時代的な制約があります。