在使用Delphi
進行三層數據庫開發時
遇到了兩個小問題
通過反復試驗
終於找出了Delphi
中的兩個小BUG並進行了修復(好像Delphi
中也有相同的BUG)
撰寫此文與大家一起分享成功的喜悅
我也是初學Delphi
文中一定存在不少說的不對的地方
還請各位朋友多多指正
BUG
傳參時中文被截斷的問題
BUG再現的方法
後台用SQL Server
裡面有一個XsHeTong表用於試驗
您可以根據您的實際情況進行調整
先創建一個數據服務器
新建項目
創建一個遠程數據模塊
上面放置ADOConnection
ADODataSet
DataSetProvider各一
並做好相應設置
其中ADODataSet的ComamndText留空
並把它的Option中的poAllowCommandText設置為True
編譯運行
再創建客戶端程序
新建項目
在窗體上放置DCOMConnection
連上前面上創建的數據服務器
再放置一個ClientDataSet
把它的連接設成這裡的DCOMConnection
並設置它的ProviderName為上面的服務器上的DataSetProvider的名字
最後放置DataSource和DBGrid各一並作相應設置用於查看結果
再放置一Button用於測試
在Button的OnClick中寫下類似於下面的代碼(這裡我用了XsHeTong的表和它的兩個字段HTH(char
)
GCMC(varchar
)
您可以根據你的實際測試情況進行調整)
with ClientDataSet
do
begin
Close;
CommandText :=
Insert Into XsHeTong(HTH
GCMC) values(:HTH
:GCMC)
;
Params[
]
AsString :=
;
Params[
]
AsString :=
會截斷的中文字
;
Execute;
Close;
CommandText :=
Select * from XsHeTong
;
Open;
end;
運行程序
點擊按鈕
看到記錄被插入了
可惜結果並不正確
會截斷的中文字
變成了
會截斷
但沒有中文的
倒是正確的插入了
BUG分析與修復
為了對照起見
我試著直接用一個ADOConnection和ADOCommand
ADOTable進行C/S構架測試
結果是正確的
中文字不會被切斷
這說明了此BUG只在三層構架上出現
用SQL Server事件探查器探查提交到SQL Server上運行的語句
發現兩層構架與三層構架的情況有以下不同
兩層構架
exec sp_executesql N
Insert into XsHeTong(HTH
GCMC) values(@P
@P
)
N
@P
varchar(
)
@P
varchar(
)
會截斷的中文字
三層構架
exec sp_executesql N
Insert into XsHeTong(HTH
GCMC) values(@P
@P
)
N
@P
varchar(
)
@P
varchar(
)
會截斷
顯然
兩層構架時
參數的長度是按實際庫結構傳的
三層構架時
參數長度是按實際參數的字符串長度傳的
而實際字符串長度又似乎是算錯了
沒有把一個中文當兩個字符長度處理
沒有辦法只好進行跟蹤調試
為了調試Delphi的VCL庫
需要在工程選項的
Compiler Options
中選上
Use Debug DCUs
先跟蹤客戶端程序
ClientDataSet
Execute後
先後經歷了TCustomClientDataSet
Exectue
TCustomeClientDataSet
PackageParams
TCustomClientDataSet
DoExecute等一系列函數
一直到AppServer
AS_Execute(ProviderName
CommandText
Params
OwnerData); 把請求提交到服務器均沒有什麼異常情況
看來問題出在服務器端
對服務器進行跟蹤
反復試驗後
我把重點落在了TCustomADODataSet
PSSetCommandText函數身上
經過反復細致的跟蹤
目標越來越精確
TCustomADODataSet
PSSetParams
TParameter
Assign
TParameter
SetValue
VarDataSize
終於找到了BUG的源頭
VarDataSize函數
下面是它的代碼
function VarDataSize(const Value: OleVariant): Integer;
begin
if VarIsNull(Value) then
Result :=
else if VarIsArray(Value) then
Result := VarArrayHighBound(Value
) +
else if TVarData(Value)
VType = varOleStr then
begin
Result := Length(PWideString(@TVarData(Value)
VOleStr)^); //出問題的行
if Result =
then
Result :=
;
end
else
Result := SizeOf(OleVariant);
end;
就是在這個函數中計算實參的長度的
它把Value中的值取出地址
並把它作為一個WideString的指針去求字符串長度
結果就導致了
會截斷的中文字
這個字符串的長度變成了
而不是
問題找到了
解決起來也就不困難了
只要簡單的把
Result := Length(PWideString(@TVarData(Value)
VOleStr)^); //出問題的行
改成
Result := Length(PAnsiString(@TVarData(Value)
VOleStr)^); //沒問題了
就可以了
但是這樣就會導致求英文字符串的長度時長度被加倍了
所以也可以把這一行改成
Result := Length(Value);
這樣
不管是中文還是英文還是中英混合的字符串就都可求得正確的長度了
這就我至今仍百思不解的問題
為什麼Borland要繞個圈子通過指針去求參數值的長度呢?哪位朋友知道的話還請給我解釋一下
非常感謝!
有些朋友可能會有疑問
為什麼在不通過三層構架來做的時候不產生這個字符串被截斷的問題呢?答案並不復雜
在直接通過ADOCommand來向SQL Server發送命令時
它是按表結構來決定參數長度的
它會先向SQL Server發一條
SET FMTONLY ON select HTH
GCMC from XsHeTong SET FMTONLY OFF
來獲取表結構
而在三層構架下
TCustomADODataSet內部雖然也是用TADOCommand對象來發命令
但它卻在取得表結構的後
並不用這個值來作為傳參長度
而是重新去按實際參數來計算長度
結果就導致了錯誤
BUG
ClientDataSet的Lookup字段的問題
BUG再現的方法
新建工程
在上面放置兩個ClientDataSet
分別為cds
和cds
它的數據來源任意
其中cds
為主數據集
在裡面增加一個新的Lookup字段
這個Lookup字段根據cds
中的一個字符型的字段值到cds
中找出對應值來
運行程序
一般來說是正常的
但是一旦cds
的被Lookup字段中的值出現了一個單引號
(您可以修改或新增一條記錄
輸入單引號試試)
立即會導致出錯
Unterminated string constant(未結束的字符串常量)
BUG分析與修復
這個BUG的產生原因要比上一個明顯得多
一定是沒有正確處理單引號帶來的副作用引起的
同樣的
我們來跟蹤VCL的源碼
運行程序
出錯時打開Call Stack窗口(在View
>Debug Windows)菜單中
查看函數調用情況
前面的一些調用是顯而易見的
沒有問題
我們從跟Lookup有關的地方開始查原因
第一個與Lookup有關的函數調用是TField
CalcLookupValue
我們在這個函數中設置斷點
重新運行程序
中斷下來後
進行單步調試
TCustomClientDataSet
Lookup
>TCustomClientDataSet
LocateRecord
經過上面的幾次函數調用
很快的
我們就把目標定在了LocateRecord過程中
在這個過程中
它根據Lookup字段的設置情況
生成相應的過濾條件
然後到目標數據集中把對應的值找到
錯就錯在過濾條件的生成上了
比如
我們要按cds
中Cust字段(假設是
)的值到cds
中按CustID字段值找到對應的CustName字段值
那生成的條件就應該是[CustID] =
但如果Cust的值是aa
bb
按生成的條件就會變成[CustID] =
aa
bb
顯然導致了一個未結束的字符串常量
通常我們解決單引號中又出現單引號的情況
只需把引號中的引號寫兩就行了
這裡也是一樣
只要讓生成的條件變成[CustID] =
aa
bb
就不會出錯了
所以可以這樣修改源代碼
在LocateRecord過程中找到下面的代碼
ftString
ftFixedChar
ftWideString
ftGUID
if (i = Fields
Count
) and (loPartialKey in Options) then
ValStr := Format(
%s*
[VarToStr(Value)]) else
ValStr := Format(
%s
[VarToStr(Value)]);
改成
ftString
ftFixedChar
ftWideString
ftGUID:
if (i = Fields
Count
) and (loPartialKey in Options) then
ValStr := Format(
%s*
[ StringReplace(VarToStr(Value)
[rfReplaceAll])])
else
ValStr := Format(
%s
[ StringReplace(VarToStr(Value)
[rfReplaceAll])]);
也就是在生成過濾條件字符串時把條件的過濾值中的單引號全部一個變兩
為了確保這樣修改的正確性
我查看了TCustomADODataSet中的對應的LocateRecord過程(在用TADODataSet中的Lookup字段時不會因單引號出錯
只在用TCustomClientDataSet時有這樣的情況)
它的處理方法與TCustomClientDataSet稍有不同
它是通過GetFilterStr函數來構造過濾條件的
但在GetFilterStr中
它正確處理了單引號的問題
所以這樣來看
沒有在TCustomClientDataSet的LocateRecord中正確處理單引號的問題
確實是Borland一個不大不小的疏漏
From:http://tw.wingwit.com/Article/program/Delphi/201311/8438.html