# 【Delphi】マルチスレッドアプリケーションでの IBX の利用 --- tags: Delphi programming Pascal InterBase objectpascal created_at: 2022-02-23 updated_at: 2022-02-25 --- # はじめに 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)](./a51df59c34a6c859296b.md) # 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](./images/93f1ee8a-ae25-9cdf-246c-40ff4a3b73cb.png) ### (5) 配置したコントロールのプロパティを変更する **Label1** | プロパティ | 値 | |:---|:---| |Caption|実行回数:| **Label2** | プロパティ | 値 | |:---|:---| |Caption|実行時間:| **Label3** | プロパティ | 値 | |:---|:---| |Caption|—| **Label4** | プロパティ | 値 | |:---|:---| |Caption|—| **CheckBox1** | プロパティ | 値 | |:---|:---| |Caption|コネクションプーリング| **Button1** | プロパティ | 値 | |:---|:---| |Caption|実行| 各プロパティの値を変更後の画面イメージは、下図の通りです。 ![image.png](./images/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](./images/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](./images/98d239d2-255a-10a6-16b5-33fe66a81ce1.png) ツールバー(上図)の `[実行]` ボタン、または、キーボードの`〔F9〕`ボタンを押します。 ### (15) [実行]ボタンを押す `[実行]` ボタンを押すと、クエリーが 500 回実行され [^3]、実行時間 (ms) が表示されます。 ![image.png](./images/5cc160a0-8396-c3fb-7f2c-00dab3713d05.png) ### (16) コネクションプーリングにチェックを入れて、[実行] ボタンを押す 今度は、コネクションプーリングにチェックを入れて、[実行] ボタンを押してください。同様にクエリーが 500 回実行され [^3]、実行時間 (ms) が表示されます。 ![image.png](./images/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](./images/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](./images/83536427-e70c-c8d4-3491-0eb0a243338d.png) Delphi 4 には、これの元となった BDE ベースの MTS Pool デモがありますが、こちらは "当時は" 正しく動作していたのかもしれません (NT4.0 の頃)。 ![image.png](./images/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)](./e8c1ff3a4c74e4c2a4f3.md) [^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 版ではローカル接続しかできないという前時代的な制約があります。