2026/09/30

翻出 Delphi dbExpress 20 年的老舊 Bug

 作者:吳祐賓  

 


 

 

先說結論,此 Bug 有兩個解決方案:

  1. 自製 Bug Fixed;直接修正 Data.DBXCommon.pas
  2. 使用 Devart DBX Driver 強制避開

 

一行 TSQLConnection.ExecuteDirect,讓我運行多年的應用程式經歷 20 年一遇的 Bug,更幽默的是,這 Bug 放到 2026 年最新的 D13.2,仍舊存在。

 

由於現在習慣全寫 T-SQL 處理邏輯問題,較少使用 Delphi 資料集元件,為了加快 SQL 指令傳遞的效率,我選擇使用 TSQLConnection.ExecuteDirect,以執行我那高達 5 Mb 的巨型 SQL 檔。

 

 

發現問題的場合

 

一開始執行的很順利,但近幾個月,使用者頻頻回報說有資料遺失的情況。然而,我的 T-SQL 指令是這樣的:

 


SET XACT_ABORT ON;
SET NOCOUNT ON;

BEGIN TRY
    BEGIN TRANSACTION;

    ...

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- 判斷若有未完成的交易則進行復原
    IF @@TRANCOUNT > 0
    BEGIN
        ROLLBACK TRANSACTION;
    END;

    -- 重新拋出錯誤資訊供呼叫端捕捉
    THROW;
END CATCH;

 

 

若是出錯,也會回傳錯誤才是,但實際上程式的反應是:

 

存檔 -> 沒錯誤 -> Commit -> 資料消失

 

 

相同指令,放到 SSMS 去跑,一切正常。

 

嗯,不出意外的,又出意外了

 

 

尋找源頭 


使用 AI 查了下我的 T-SQL,回報的內容重點在:

 

ExecuteDirect 是用來執行不回傳資料集的 SQL;批次中途出現真正的 SELECT result set,DBExpress 驅動可能只處理到第一個結果集、取消或中止剩餘批次
 

看來是 ExecuteDirect 中觸發了 MARS(Multiple Active Result Sets,多重作用結果集)造成的問題。

 

但 Devart 在說明中確實提到它家的 DBX Driver 有支援 MARS 功能,這或許是 Devart 的 Bug 也說不定。

 

於是我使用以下 SQL 範例進行測試:


LCommand.Text :=
  'SET NOCOUNT ON;' + sLineBreak +
  'SELECT 1 AS n;' + sLineBreak +
  'SELECT 2 AS n;' + sLineBreak +
  'SELECT 3 AS n;';

 

使用 TSQLStoredProc 進行測試,和 Native DBX Driver 進行比較,結果 Native DBX Driver 就是只有一包,沒有其它的內容回來,但 Native DBX Driver 早就被宣告不會更新,所以有這結果不意外。

倒是 Devart DBX Driver 出現無限迴圈的結果。因為 TSQLStoredProc 已是包殼後的高階元件,後來改用 TDBXCommand 的底層物件測試,結果也是相同。

 


 

 


procedure TForm2.MultiResultSetTest(ACn: TSQLConnection; AMemo: TMemo);
var
  LCommand: TDBXCommand;
  LReader: TDBXReader;
  LLog: TStringList;
  LResultSetCount: Integer;
  LResultRowCount: Integer;
  LCurrentRowCount: Integer;
begin
  LCommand := nil;
  LReader := nil;
  LLog := TStringList.Create;

  try
    LResultSetCount := 0;
    LResultRowCount := 0;

    ACn.Connected := True;

    LCommand := ACn.DBXConnection.CreateCommand;
    LCommand.CommandType := TDBXCommandTypes.DbxSQL;
    LCommand.Text :=
      'SET NOCOUNT ON;' + sLineBreak +
      'SELECT 1 AS n;' + sLineBreak +
      'SELECT 2 AS n;' + sLineBreak +
      'SELECT 3 AS n;';

    LCommand.Prepare;
    LReader := LCommand.ExecuteQuery;

    while (LReader <> nil) and (LResultSetCount < 9) do
    begin
      Inc(LResultSetCount);
      LCurrentRowCount := 0;

      while LReader.Next do
      begin
        LLog.Add('First Value: ' + LReader.Value[0].AsString);
        Inc(LCurrentRowCount);
        Inc(LResultRowCount);
      end;

      LLog.Add(Format(
        'Result set #%d: %d row(s)',
        [LResultSetCount, LCurrentRowCount]));

      FreeAndNil(LReader);

      // Directly test the low-level DBX API.
      LReader := LCommand.GetNextReader;
    end;

    LLog.Add('');
    LLog.Add(Format(
      'ResultSetCount = %d',
      [LResultSetCount]));

    LLog.Add(Format(
      'ResultRowCount = %d',
      [LResultRowCount]));

    AMemo.Lines.Assign(LLog);
  finally
    FreeAndNil(LReader);
    FreeAndNil(LCommand);
    LLog.Free;
  end;
end;

 

真相是,不只 Native Driver 有問題,Framework 也有問題

 

自 Delphi XE DBX Driver 支援到 SQL Server Native Client (SNAC) 2008 版本後,就沒有再往後更新,有 MARS 支援,設定了卻沒有任何作用。

 

Devart 有功能,就代表 Native DBX Driver 是有問題的。

 

Devart 的無窮迴圈問題,是出在 Next 取得 Reader 時, 因為 Reader 指向錯誤造成的問題,我直接把要修正 Data.DBXCommon.pas 內容列出:

 


--- Embarcadero/Data.DBXCommon.pas
+++ Fixed/Data.DBXCommon.pas
@@
 function TDBXCommand.GetNextReader: TDBXReader;
 begin
   CloseReader;
   Result := DerivedGetNextReader;
   if Assigned(Result) then
-    Result.FCommand := Self;
+  begin
+    Result.FCommand := Self;
+    FLastReader := Result;
+  end;
 end;

 

而且 DBXCommon 單元藕合度極低,所以可以複製到使用 DBX 元件的專案下獨立編譯使用,要另外拿 dcu 去 fixed 官方版本也是可以的。

 

以上是修復 DBX framework 的內容。前提是你要使用 3rd Driver 才有效。


 

Devart 的靈活解方:用 Driver 解 Framework

 

經過 Devart 研發單位的調查,發現 DBX Framework 這個 Bug 自 Delphi 2007 的 DBX4 開始,一直到 2026 年的 D13.2,一直都在。

 

與其修復 Framework,使用 Driver 也有能力繞過此 Bug。

 

目前已知在 2026 年 10 月 9.5.1 版就會提供此次修正。 

 

真令人期待!

 

 

而這段期間我的選擇是

 

回到一開始,我就只是要使用 TSQLConnection.ExecuteDirect,我也不用什麼高階功能。

 

既然都使用 DBX 底層元件,又是使用 SQL Server。

 

所以我選 ADO 底層元件 Command / RecordSet 硬繞!

 

 

沒有留言:

張貼留言