今更ながら BDE (Borland Database Engine)
未だに使われる事もあります。正直な話、
- 最新バージョンは Delphi6から 付属してる 5.2 だけど...?
- Paradox/dBASE のデータベースアプリを新規に作る事ってあるの?
- 組み込みなら、BlackFishSQL や Firebird (Embedded) があるじゃない?
- 4.x と 5.x のBDE が混在したらどうするの?
- エリアスが重複したらどうするの?
- 配布が面倒じゃない?
- マルチコア/マルチプロセッサのPCでエラーになる事があるけどいいの?
- Vista 以降だと実行に管理者権限が必要な事があるよ?
- 64bit アプリではそもそも BDE が使えないよ?
と、業務アプリプログラミングが本業の私が言ってみます。
新規案件で BDE を使うのは流石にどうかと思うのですが、古いPCからのデータ移行で Paradox/xBASE を扱う場合には BDE を使うのが手っ取り早いのも事実です。業務で使われている MS-DOS マシンには xBASE のデータがゴロゴロしてたりしますね(Btrieveもありますけどね)。
あ、xBASE というのは、dBASE またはその互換アプリケーション の事です。昔は "Quick Silver" なんていう dBASE 互換アプリケーションがあったんですよ。Microsoft も xBASE 製品を出してまして、"FoxPro/Visual FoxPro" ってのがあります。日本では発売されなかったようですけどね。他の xBASE 製品には "dBXL" とか "ARAGO for Windows" なんてのがあります。
BDE の概要
BDE は、
- dBASE
- Paradox
- Microsoft Access
- FoxPro
- ASCII
のデータベースを特殊なドライバなしでアクセスできます。
このうち、dBASE / Paradox / FoxPro は SQL 型データベースではありませんが、SQL-92 サブセットの "Borland Local SQL" を利用する事ができます (イメージ的には "BDE が SQL を翻訳してくれている" 感じです)。利用できる構文等、ローカル SQL の概要は "%ProgramFiles%\Common Files\Borland Shared\BDE" にある LOCALSQL.HLP を参照して下さい。
- InterBase
- Microsoft SQL SERVER
- ORACLE
- SYBASE
- INFORMIX
- DB2
これらのデータベースは "SQL Link" と呼ばれるドライバを介してアクセスします。BDE に ODBC を接続して各種データベースにアクセスする事も可能です。
BDE の情報(公式)
"CodeGear Delphi 2009 および C++Builder 2009 のリリース ノート" にも書いてありますが、Vista 環境下ではデータベースファイルをドライブルート直下("C:\"等)に配置してはいけません。
BDE / SQL Link の配布
BDE / SQL Link は Borland 製品で作成されたアプリケーションと共に配布可能です。詳しい内容は BDE のインストールフォルダにある、"bdedeploy.txt" をお読み下さい。
ここからが本トピックの主題です。問題は "どうやって配布するのか?" です。
BDE の単体配布
これは明らかにライセンス違反です。ですが、この方法を知らない事にはインストーラ/インストールプログラムを作る事ができません。
- 既存の BDE フォルダから BDE 単体配布用パッケージを得る。
- "%ProgramFiles%\Common Files\Borland Shared\BDE\" を適当な場所にコピーする("BDE_TEMP"とか)。
- コピーしたフォルダから、"IDAPI.CFG" を削除。
- コピーしたフォルダにある "bdeinst.cab" を解凍し、"bdeinst.dll" を得る。
- これを BDE をインストールしたい PC へコピー。
- コピーしたフォルダで "regsvr32 bdeinst.dll" を実行。
- アンインストールするには、コピーしたフォルダで "regsvr32 /u bdeinst.dll" を実行し、"%ProgramFiles%\Common Files\Borland Shared\BDE\" を削除。
- "BDE マージモジュール" を使う。
- "BDE マージモジュール" を BDE をインストールしたい PC へコピー。
(Delphi 2009 の場合、"BDE マージモジュール" は インストールDVDの "Info\BDEMergeModule\jp" にあります)
- コピーしたフォルダで "msiexec /i BDE_???.msm" を実行。
- アンインストールするには、コピーしたフォルダで "msiexec /x BDE_???.msm" を実行。
dBASE / Paradox / Access 等のネイティブドライバで動作するものを使うのなら上記だけでもいいのですが、SQL-Link ドライバが必要な場合には、別途 SQL-Link を配布しなくてはなりません。
- 開発環境の "%ProgramFiles%\Common Files\Borland Shared\BDE\" にある bdedeploy.txt を読み、必要な SQL-Link ドライバファイルを調べる。
- 調べた SQL-Link ドライバファイルをターゲット PC の "%ProgramFiles%\Common Files\Borland Shared\BDE\" へコピーする。
- 開発環境のレジストリ [HKEY_LOCAL_MACHINE\SOFTWARE\Borland\Database Engine\Settings\DRIVERS] を調べ、目的の DB 用のレジストリを探す (Interbase なら "INTRBASE")。
64bit 環境では、レジストリキーが [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Borland\Database Engine\Settings\DRIVERS] にリダイレクトされる事があります。
- 調べたレジストリエントリをターゲット PC のレジストリに書き込む。
多少面倒ですが解ってしまえば難しくはありません。
※ 64bit Windows で 32bit DLL を登録するための regsvr32 は %systemroot%\SysWoW64 にあります。%systemroot%\System32\にあるのは 64bit DLL 用です。
BDE をインストーラに含める
各製品のマニュアルを参照して下さい(^^; 既に単体配布の方法が判っているのですから、どんなインストーラでも BDE を含める事は可能です。BDE を配布するなら、エリアスを作れる InstallShield が一番簡単です。他のインストーラではエリアスの作成が面倒です。
エリアスをアプリケーション内で設定する
エリアスをアプリケーションから設定してしまうというテもあります。この方法なら、インストーラでエリアスを設定する手間が必要ありません。データベースが Paradox/dBASE/ASCII ならエリアスの作成は非常に簡単です。
uses
..., DBTables;
procedure AddStdAlias(Alias, Driver, Path: String);
begin
if not Session.IsAlias(Alias) then
begin
Session.AddStandardAlias(Alias, Path, Driver);
Session.SaveConfigFile;
end;
end;
|
この関数を利用して、
procedure TForm1.FormShow(Sender: TObject);
begin
OnShow := nil;
AddStdAlias('TEST_PARADOX', 'PARADOX', IncludeTarilingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'DATA');
end;
|
こんな感じで使います。例では Paradox のエリアス "TEST_PARADOX" を作り、パスを "<EXEの存在するフォルダ>\DATA" に設定しています。AddStdAlias() の第2引数(Driver)に利用できる文字列は、
のいずれかです。その他のドライバを使う場合はパラメータを指定しなくてはならないので少々面倒です。
uses
..., DBTables;
procedure AddSqlAlias(Alias, Driver: String; List: TStrings);
begin
if not Session.IsAlias(Alias) then
begin
Session.AddAlias(Alias, Driver, List);
Session.SaveConfigFile;
end;
end;
|
この関数を利用して、
procedure TForm1.FormShow(Sender: TObject);
const
DriverName = 'INTRBASE';
var
Params: TStringList;
begin
OnShow := nil;
Params := TStringList.Create;
try
Session.GetDriverParams(DriverName, Params);
Params.Values['USER NAME' ] := 'SYSDBA';
Params.Values['SERVER NAME'] := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'DATA\data.fdb';
AddSqlAlias('TEST_Interbase', DriverName, Params);
finally
Params.Free;
end;
end;
|
こんな感じで使います。例では Interbase のエリアス "TEST_Interbase" を作り、"SERVER NAME"パラメータ を "<EXEの存在するフォルダ>\DATA\data.fdb" に設定しています。第2引数(Driver)に利用できる文字列や、第3引数(List)に格納できるパラメータ及びパラメータの値は "BDE Administrator" で確認して下さい。
エリアスが既に存在する場合にエリアスのパラメータを変更したい事もあるでしょう。以下にエリアスを簡単に操作できるクラスを用意してみました。
unit uBDE_CONFIG;
interface
uses
Classes, BDE, DBTables;
type
TBDEConfig = class(TObject)
private
FDriverName: string;
FParams: TStringList;
FAliasName: string;
procedure SetAliasName(const Value: string);
public
// Constructor / Destructor
constructor Create(aDriverName: String);
destructor Destroy; override;
// Methods
property AliasName: string read FAliasName write SetAliasName;
procedure Apply(Save: Boolean = True);
procedure DeleteAlias(Save: Boolean = True); overload;
class procedure DeleteAlias(aAliasName: string; Save: Boolean = True); overload;
function IsAlias: Boolean; overload;
class function IsAlias(aAliasName: string): Boolean; overload;
// Properties
property DriverName: string read FDriverName;
property Params: TStringList read FParams write FParams;
end;
implementation
{ TBDEConfig }
constructor TBDEConfig.Create(aDriverName: String);
begin
FParams := TStringList.Create;
FDriverName := aDriverName;
if FDriverName = '' then
FDriverName := szCFGDBSTANDARD;
end;
destructor TBDEConfig.Destroy;
begin
FParams.Free;
inherited;
end;
procedure TBDEConfig.Apply(Save: Boolean);
begin
if Session.IsAlias(FAliasName) then
Session.ModifyAlias(FAliasName, FParams)
else
Session.AddAlias(FAliasName, FDriverName, FParams);
if Save then
Session.SaveConfigFile;
end;
procedure TBDEConfig.DeleteAlias(Save: Boolean);
begin
Self.DeleteAlias(FAliasName, Save);
end;
class procedure TBDEConfig.DeleteAlias(aAliasName: string; Save: Boolean);
begin
Session.DeleteAlias(aAliasName);
if Save then
Session.SaveConfigFile;
end;
function TBDEConfig.IsAlias: Boolean;
begin
result := Self.IsAlias(FAliasName);
end;
class function TBDEConfig.IsAlias(aAliasName: string): Boolean;
begin
result := Session.IsAlias(aAliasName);
end;
procedure TBDEConfig.SetAliasName(const Value: string);
begin
FAliasName := Value;
if Session.IsAlias(FAliasName) then
Session.GetDriverParams(FDriverName, FParams)
else
begin
FParams.Clear;
if FDriverName = szCFGDBSTANDARD then
begin
FParams.Values[szCFGDBDEFAULTDRIVER] := szPARADOX;
FParams.Values[szCFGDBENABLEBCD ] := szCFGFALSE;
end;
end;
end;
end.
|
dBASE のエリアスを設定するコードは以下のようになります。
uses
..., BDE, uBDE_CONFIG;
var
BDEConfig: TBDEConfig;
DatabsePath: string;
begin
BDEConfig := TBDEConfig.Create(''); // Use STANDARD Driver
try
BDEConfig.AliasName := 'STANDARD_TEST';
DatabsePath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'DATA';
with BDEConfig.Params do
begin
// Values['DEFALT DRIVER'] := 'DBASE'; // DEFALT DRIVER = DBASE
// Values['PATH' ] := DatabsePath; // PATH = APPPath
Values[szCFGDEFDRV] := szDBASE; // DEFALT DRIVER = DBASE
Values[szCFGDBPATH] := DatabsePath; // PATH = APPPath
end;
BDEConfig.Apply; // Create or Modify Alias
finally
BDEConfig.Free;
end;
end;
|
エリアスが存在しない場合にはエリアスを新規作成し、存在する場合には指定したパラメータを更新します。
以下は、同様に Interbase のエリアスを設定するコードのサンプルです。
uses
..., BDE, uBDE_CONFIG;
var
BDEConfig: TBDEConfig;
ServerPath, DatabasePath, DatabaseName: string;
begin
BDEConfig := TBDEConfig.Create('INTRBASE'); // Use INTERBASE Driver
try
BDEConfig.AliasName := 'INTERBASE_TEST';
ServerPath := '127.0.0.1';
DatabasePath := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'DATA';
DatabaseName := 'DATA.IDB';
with BDEConfig.Params do
begin
// Values['USER NAME' ] := 'SYSDBA';
// Values['SERVER NAME'] := Format('%s:%s\%s', [ServerPath, DatabasePath, DatabaseName]);
Values[szUSERNAME ] := 'SYSDBA';
Values[szSERVERNAME] := Format('%s:%s\%s', [ServerPath, DatabasePath, DatabaseName]);
end;
BDEConfig.Apply; // Create or Modify Alias
finally
BDEConfig.Free;
end;
end;
|
トラブルシューティング
- エラー $2A04: Operation not applicable.
詳細は "マルチプロセッサマシン上で BDEを使用する (CDN)" をお読みください。結論から言うとプロジェクトファイルに以下のように記述するようにします。実質 1 行だけ追加すればいいです。
uses
...
Windows,
...
SetProcessAffinityMask(GetCurrentProcess, 1); // CPU 0 のみを使用
|
こう記述する事によって、マルチプロセッサマシンでも単一の CPU しか利用しないようになります 。タスクマネージャのプロセスタブで対象のアプリケーションを右クリックして "関係の設定..." を表示するとアプリケーションのプロセスが利用している CPU を確認 / 設定できます。上記コードを記述した場合には、CPU 0 以外は利用不可になっているハズです。
- エラー $3E06: Cannot load language driver.
"SQL Link"ドライバDLL が正しくコピーされていないか、BDE が正しくセットアップされていません。ファイルが揃っているのなら "BDE の単体配布" の項目を参考にして、"regsvr32 bdeinst.dll" を実行してみて下さい。
- PDOXUSRS.NET のエラーが出る
比較的新しい Delphi の readme.html にはこう書かれています。
Windows Vista で BDE を使用している場合は、BDE が C:\<ルート> ディレクトリにファイルを書き込まないように、BDE を再構成する必要があります。
それには、管理者としてログインし、Program Files\Common Files\Borland Shared\BDE にある BDEAdmin.exe を実行します。
[Native] の下の [PARADOX] をクリックし、[NET DIR] をルート ディレクトリ以外の場所を参照するように変更します。
推奨するディレクトリは、C:\Users\Public\Documents\RAD Studio です。
|
PDOXUSRS.NET がドライブルートに生成されるのがマズいという事なので、これを ACL の制限に引っ掛からない場所へ作るようにすればいいという事です。
// ACL 対策
Session.NetFileDir := SysUtils.GetHomePath;
Session.PrivateDir := SysUtils.GetHomePath;
|
コードで書いた方が楽ですね。PDOXUSRS.NET 生成先をアプリケーションデータパス (%APPDATA%) ではなく "ExtractFilePath(ParamStr(0))" のようにして EXE と同じパスにしてもいいような気がしますが、これだとインストーラがあって "%ProgramFiles%" 以下にインストールされた場合には、ACL の制限に引っ掛かってしまいます。
- エラー $2503: Insufficient disk space.
HDD は充分すぎるほど空いているのにディスク容量不足のエラーが出る件は、QC#7089 です。絶賛放置プレイ中です。以下の関数を使えば回避できる場合があります。
procedure BDECreateDmyFile;
// BDE は空き容量を Integer で計算しているので、
// BDE から見た空き容量が少なくなるとエラーになる (QC#7089)
var
SystemDir: array [0..MAX_PATH] of Char;
C: Char;
FSize, BSize, TSize: Int64;
AdjustFile: String;
TargetDriveIdx: Byte;
FileHandle: Integer;
begin
// 調整ファイルは %APPDATA% に生成
AdjustFile := IncludeTrailingPathDelimiter(SysUtils.GetHomePath) + 'adjust.tmp';
// 調整ファイルを一旦削除
DeleteFile(AdjustFile);
// システムフォルダを得る
GetSystemDirectory(SystemDir, SizeOf(SystemDir));
// システムドライブの Index (A=1, B=2, C=3 ...)
TargetDriveIdx := Byte(SystemDir[0]) - Ord('A') + 1;
// 実空き容量
FSize := DiskFree(TargetDriveIdx);
// BDE から見た空き容量
BSize := (FSize mod MaxInt);
// BDE から見た空き容量が 150MB 以下かつ、
// 実空き容量が 2GB を超えていたらダミーファイルを作る
if (BSize < (150 * 1024 * 1024)) and (FSize > MaxInt) then
begin
FileHandle := FileCreate(AdjustFile);
// BDE から見た空き容量が 150MB を超えるように
// 調整されたファイルサイズ
TSize := BSize + 2048;
FileSeek(FileHandle, TSize, 0);
C := #$0000;
FileWrite (FileHandle, C, 1);
FileClose (FileHandle);
end;
end;
|
BDE がディスクの空き容量を Integer で取得しているので、"実ディスク容量 mod MaxInt" が少ないと、空き容量が充分に残っていてもディスク容量不足のエラーが出ます。コードでは %AppData% にダミーファイルを作ってBDE から見た空き容量を調整しています。
...ですが、この方法は "%APPData% がシステムドライブに存在する" という前提になっています。%APPData% がシステムドライブ以外に存在する場合には対処できません (実は変更可能)。じゃ、%TEMP% ならどうかというと、これまたテンポラリ位置は変更可能です。"<システムドライブ>:\" とか <システムドライブ>:\Windows\TEMP 等は UAC が有効だと書き込み不可能です。
function BDECreateDmyFile: string;
// BDE は空き容量を Integer で計算しているので、
// BDE から見た空き容量が少なくなるとエラーになる (QC#7089)
const
TMPFILENAME = 'adjust.tmp';
var
SystemDir: array [0..MAX_PATH] of Char;
C: Char;
FSize, BSize, TSize: Int64;
AdjustFile, AF1, AF2, TmpDir: string;
TargetDriveIdx: Byte;
FileHandle: Integer;
begin
// システムフォルダを得る
GetSystemDirectory(SystemDir, SizeOf(SystemDir));
// 調整ファイル1 (%APPDATA%)
AF1 := IncludeTrailingPathDelimiter(SysUtils.GetHomePath) + TMPFILENAME;
// 調整ファイル1 を一旦削除
if FileExists(AF1) then
DeleteFile(AF1);
// 調整ファイル2 ("<システムドライブ>:\BDE_TEMP")
TmpDir := 'X:\BDE_TEMP\';
TmpDir[1] := SystemDir[0]; // ドライブ文字列をシステムドライブ文字列で置換
AF2 := TmpDir + TMPFILENAME; // フルパス名を得る
// 調整ファイル2 を一旦削除
if FileExists(AF2) then
DeleteFile(AF2);
// %AppData% がシステムドライブじゃなかったら
if AF1[1] <> SystemDir[0] then
begin
// "<システムドライブ>:\BDE_TEMP" を生成
if not DirectoryExists(TmpDir) then
CreateDir(TmpDir);
// 調整ファイルは "<システムドライブ>:\BDE_TEMP" に作る
AdjustFile := AF2;
end
else
begin
// "<システムドライブ>:\BDE_TEMP" を削除
if DirectoryExists(TmpDir) then
RemoveDir(TmpDir);
// 調整ファイルは %APPDATA% に作る
AdjustFile := AF1;
end;
// システムドライブの Index (A=1, B=2, C=3 ...)
TargetDriveIdx := Byte(SystemDir[0]) - Ord('A') + 1;
// 実空き容量
FSize := DiskFree(TargetDriveIdx);
// BDE から見た空き容量
BSize := (FSize mod MaxInt);
// BDE から見た空き容量が 150MB 以下かつ、
// 実空き容量が 2GB を超えていたらダミーファイルを作る
if (BSize < (150 * 1024 * 1024)) and (FSize > MaxInt) then
begin
FileHandle := FileCreate(AdjustFile);
// BDE から見た空き容量が 150MB を超えるように
// 調整されたファイルサイズ
TSize := BSize + 2048;
FileSeek(FileHandle, TSize, 0);
C := #$0000;
FileWrite (FileHandle, C, 1);
FileClose (FileHandle);
end;
result := AdjustFile;
end;
|
上記コードは苦肉の策です。%AppData% がシステムドライブでない場合には "<システムドライブ>:\BDE_TEMP" にダミーファイルを生成します。戻り値としてダミーファイルのフルパス名を返します。
この問題を解決する最も手軽な方法は "Patch for BDE 'Insufficient disk space' problem." を使う事でしょう。
- アーカイブを解凍したら、その中にある FixBDE4GbBug.pas をプロジェクトフォルダにコピーする。
- プロジェクトファイルの uses の先頭に "FixBDE4GbBug" を追加してビルドする。
- Delphi アプリケーション以外では "FIX4GBug.dll" を静的リンクし、アプリケーションと共に配布する。
GetDiskFreeSpaceA() を別関数にリダイレクトしているようです。テンポラリファイルを作る方法でもいいのですが、システムドライブにテンポラリフォルダがあったりすると、タイミングによってはやっぱりエラーになる事がありますので、DLL を別途配布できるのなら "Patch for BDE 'Insufficient disk space' problem." のやり方のほうが確実でしょう。
- エラー $210D, $2501: An error occurred while attempting to initialize the Borland Database Engine
Vista 以降の OS でこのエラーが発生します。BDE Administrator の [環境設定] タブを開き、[System | INIT] にある SHAREDMEMLOCATION の値を 3BDE 等に変更します。SHAREDMEMLOCATION に設定できる値は 1000 (0x10000000) ~ 7F00 (0x7F000000) です (NT 系の Windows の場合)。デフォルトでは 6BDE (0x6BDE0000) に設定されています。
この問題は ASLR が 0x50000000 ~ 0x78000000 の領域を使用する事により発生するようです。つまり、SHAREDMEMLOCATION の値を 5000~7800 の間で設定してはいけません。
おまけ
Btrieve の話をしてしまったので、補足。
Btrieve は、現在 "Pervasive.SQL / Pervasive PSQL / PSQL SUMMIT" と名を変えています。純粋な Btrieve とはもはや別物の感がありますが、"Pervasive.SQL / Pervasive PSQL / PSQL SUMMIT" では Btrieve を扱うことができまして、Delphi からは ActiveX 経由で "Pervasive.SQL / Pervasive PSQL / PSQL SUMMIT" を操作できます。詳しくは、"Pervasive.SQL を Delphi で使用 (PERVASIVE)" を参照して下さい。