Delphi中兩個(gè)BUG的分析與修復(fù)
在使用Delphi 7進(jìn)行三層數(shù)據(jù)庫(kù)開(kāi)發(fā)時(shí),遇到了兩個(gè)小問(wèn)題,通過(guò)反復(fù)試驗(yàn),終于找出了Delphi 7中的兩個(gè)小BUG并進(jìn)行了修復(fù)(好像Delphi 6中也有相同的BUG),撰寫(xiě)此文與大家一起分享成功的喜悅。我也是初學(xué)Delphi,文中一定存在不少說(shuō)的不對(duì)的地方,還請(qǐng)各位朋友多多指正。
BUG1.傳參時(shí)中文被截?cái)嗟膯?wèn)題:
BUG再現(xiàn)的方法:
后臺(tái)用SQL Server 2000,里面有一個(gè)XsHeTong表用于試驗(yàn),您可以根據(jù)您的實(shí)際情況進(jìn)行調(diào)整。
先創(chuàng)建一個(gè)數(shù)據(jù)服務(wù)器:新建項(xiàng)目,創(chuàng)建一個(gè)遠(yuǎn)程數(shù)據(jù)模塊,上面放置ADOConnection、ADODataSet、DataSetPRovider各一,并做好相應(yīng)設(shè)置,其中ADODataSet的ComamndText留空,并把它的Option中的poAllowCommandText設(shè)置為T(mén)rue。編譯運(yùn)行。
再創(chuàng)建客戶端程序:新建項(xiàng)目,在窗體上放置DCOMConnection,連上前面上創(chuàng)建的數(shù)據(jù)服務(wù)器,再放置一個(gè)ClientDataSet,把它的連接設(shè)成這里的DCOMConnection,并設(shè)置它的ProviderName為上面的服務(wù)器上的DataSetProvider的名字。最后放置DataSource和DBGrid各一并作相應(yīng)設(shè)置用于查看結(jié)果,再放置一Button用于測(cè)試。
在Button的OnClick中寫(xiě)下類(lèi)似于下面的代碼(這里我用了XsHeTong的表和它的兩個(gè)字段HTH(char 15)、GCMC(varchar 100),您可以根據(jù)你的實(shí)際測(cè)試情況進(jìn)行調(diào)整):
with ClientDataSet1 do
begin
Close;
CommandText := 'Insert Into XsHeTong(HTH, GCMC) values(:HTH,:GCMC)';
Params[0].AsString := '12345';
Params[1].AsString := '會(huì)截?cái)嗟闹形淖?;
Execute;
Close;
CommandText := 'Select * from XsHeTong';
Open;
end;
運(yùn)行程序,點(diǎn)擊按鈕,看到記錄被插入了,可惜結(jié)果并不正確,“會(huì)截?cái)嗟闹形淖帧弊兂闪恕皶?huì)截?cái)唷保珱](méi)有中文的“12345”倒是正確的插入了。
BUG分析與修復(fù):
為了對(duì)照起見(jiàn),我試著直接用一個(gè)ADOConnection和ADOCommand、ADOTable進(jìn)行C/S構(gòu)架測(cè)試,結(jié)果是正確的,中文字不會(huì)被切斷。這說(shuō)明了此BUG只在三層構(gòu)架上出現(xiàn)。
用SQL Server事件探查器探查提交到SQL Server上運(yùn)行的語(yǔ)句,發(fā)現(xiàn)兩層構(gòu)架與三層構(gòu)架的情況有以下不同:
兩層構(gòu)架:
exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(15),@P2 varchar(100)', '12345', '會(huì)截?cái)嗟闹形淖?
三層構(gòu)架:
exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(5),@P2 varchar(7)', '12345', '會(huì)截?cái)?/FONT>
顯然,兩層構(gòu)架時(shí),參數(shù)的長(zhǎng)度是按實(shí)際庫(kù)結(jié)構(gòu)傳的,三層構(gòu)架時(shí),參數(shù)長(zhǎng)度是按實(shí)際參數(shù)的字符串長(zhǎng)度傳的,而實(shí)際字符串長(zhǎng)度又似乎是算錯(cuò)了,沒(méi)有把一個(gè)中文當(dāng)兩個(gè)字符長(zhǎng)度處理。
沒(méi)有辦法只好進(jìn)行跟蹤調(diào)試,為了調(diào)試Delphi的VCL庫(kù),需要在工程選項(xiàng)的“Compiler Options”中選上“Use Debug DCUs”。
先跟蹤客戶端程序,ClientDataSet1.Execute后,先后經(jīng)歷了TCustomClientDataSet.Exectue、TCustomeClientDataSet.PackageParams、TCustomClientDataSet.DoExecute等一系列函數(shù),一直到AppServer.AS_Execute(ProviderName, CommandText, Params, OwnerData); 把請(qǐng)求提交到服務(wù)器均沒(méi)有什么異常情況,看來(lái)問(wèn)題出在服務(wù)器端。
對(duì)服務(wù)器進(jìn)行跟蹤,反復(fù)試驗(yàn)后,我把重點(diǎn)落在了TCustomADODataSet.PSSetCommandText函數(shù)身上,經(jīng)過(guò)反復(fù)細(xì)致的跟蹤,目標(biāo)越來(lái)越精確:TCustomADODataSet.PSSetParams、TParameter.Assign、TParameter.SetValue、VarDataSize。終于找到了BUG的源頭:VarDataSize函數(shù),下面是它的代碼:
function VarDataSize(const Value: OleVariant): Integer;
begin
if VarIsNull(Value) then
Result := -1
else if VarIsArray(Value) then
Result := VarArrayHighBound(Value, 1) + 1
else if TVarData(Value).VType = varOleStr then
begin
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出問(wèn)題的行
if Result = 0 then
Result := -1;
end
else
Result := SizeOf(OleVariant);
end;
就是在這個(gè)函數(shù)中計(jì)算實(shí)參的長(zhǎng)度的,它把Value中的值取出地址,并把它作為一個(gè)WideString的指針去求字符串長(zhǎng)度,結(jié)果就導(dǎo)致了“會(huì)截?cái)嗟闹形淖帧边@個(gè)字符串的長(zhǎng)度變成了7,而不是14。
問(wèn)題找到了,解決起來(lái)也就不困難了,只要簡(jiǎn)單的把
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出問(wèn)題的行
改成
Result := Length(PAnsiString(@TVarData(Value).VOleStr)^); //沒(méi)問(wèn)題了
就可以了。
但是這樣就會(huì)導(dǎo)致求英文字符串的長(zhǎng)度時(shí)長(zhǎng)度被加倍了,所以也可以把這一行改成:
Result := Length(Value);
這樣,不管是中文還是英文還是中英混合的字符串就都可求得正確的長(zhǎng)度了。這就我至今仍百思不解的問(wèn)題,為什么Borland要繞個(gè)圈子通過(guò)指針去求參數(shù)值的長(zhǎng)度呢?哪位朋友知道的話還請(qǐng)給我解釋一下,非常感謝!
有些朋友可能會(huì)有疑問(wèn),為什么在不通過(guò)三層構(gòu)架來(lái)做的時(shí)候不產(chǎn)生這個(gè)字符串被截?cái)嗟膯?wèn)題呢?答案并不復(fù)雜,在直接通過(guò)ADOCommand來(lái)向SQL Server發(fā)送命令時(shí),它是按表結(jié)構(gòu)來(lái)決定參數(shù)長(zhǎng)度的。它會(huì)先向SQL Server發(fā)一條
SET FMTONLY ON select HTH,GCMC from XsHeTong SET FMTONLY OFF
來(lái)獲取表結(jié)構(gòu)。而在三層構(gòu)架下,TCustomADODataSet內(nèi)部雖然也是用TADOCommand對(duì)象來(lái)發(fā)命令,但它卻在取得表結(jié)構(gòu)的后,并不用這個(gè)值來(lái)作為傳參長(zhǎng)度,而是重新去按實(shí)際參數(shù)來(lái)計(jì)算長(zhǎng)度,結(jié)果就導(dǎo)致了錯(cuò)誤。
BUG2.ClientDataSet的Lookup字段的問(wèn)題:
BUG再現(xiàn)的方法:
新建工程,在上面放置兩個(gè)ClientDataSet,分別為cds1和cds2,它的數(shù)據(jù)來(lái)源任意,其中cds1為主數(shù)據(jù)集,在里面增加一個(gè)新的Lookup字段,這個(gè)Lookup字段根據(jù)cds1中的一個(gè)字符型的字段值到cds2中找出對(duì)應(yīng)值來(lái)。
運(yùn)行程序,一般來(lái)說(shuō)是正常的,但是一旦cds1的被Lookup字段中的值出現(xiàn)了一個(gè)單引號(hào)"'"(您可以修改或新增一條記錄,輸入單引號(hào)試試),立即會(huì)導(dǎo)致出錯(cuò): Unterminated string constant(未結(jié)束的字符串常量)。
BUG分析與修復(fù):
這個(gè)BUG的產(chǎn)生原因要比上一個(gè)明顯得多,一定是沒(méi)有正確處理單引號(hào)帶來(lái)的副作用引起的。
同樣的,我們來(lái)跟蹤VCL的源碼:
運(yùn)行程序,出錯(cuò)時(shí)打開(kāi)Call Stack窗口(在View->Debug Windows)菜單中,查看函數(shù)調(diào)用情況,前面的一些調(diào)用是顯而易見(jiàn)的,沒(méi)有問(wèn)題,我們從跟Lookup有關(guān)的地方開(kāi)始查原因,第一個(gè)與Lookup有關(guān)的函數(shù)調(diào)用是TField.CalcLookupValue,我們?cè)谶@個(gè)函數(shù)中設(shè)置斷點(diǎn),重新運(yùn)行程序,中斷下來(lái)后,進(jìn)行單步調(diào)試。
TCustomClientDataSet.Lookup->TCustomClientDataSet.LocateRecord
經(jīng)過(guò)上面的幾次函數(shù)調(diào)用,很快的,我們就把目標(biāo)定在了LocateRecord過(guò)程中,在這個(gè)過(guò)程中,它根據(jù)Lookup字段的設(shè)置情況,生成相應(yīng)的過(guò)濾條件,然后到目標(biāo)數(shù)據(jù)集中把對(duì)應(yīng)的值找到,錯(cuò)就錯(cuò)在過(guò)濾條件的生成上了。比如,我們要按cds1中Cust字段(假設(shè)是001)的值到cds2中按CustID字段值找到對(duì)應(yīng)的CustName字段值。那生成的條件就應(yīng)該是[CustID] = '001',但如果Cust的值是aa'bb,按生成的條件就會(huì)變成[CustID] = 'aa'bb',顯然導(dǎo)致了一個(gè)未結(jié)束的字符串常量。
通常我們解決單引號(hào)中又出現(xiàn)單引號(hào)的情況,只需把引號(hào)中的引號(hào)寫(xiě)兩就行了,這里也是一樣,只要讓生成的條件變成[CustID] = 'aa''bb'就不會(huì)出錯(cuò)了。所以可以這樣修改源代碼:
在LocateRecord過(guò)程中找到下面的代碼:
ftString, ftFixedChar, ftWideString, ftGUID
if (i = Fields.Count - 1) and (loPartialKey in Options) then
ValStr := Format('''%s*''',[VarToStr(Value)]) else
ValStr := Format('''%s''',[VarToStr(Value)]);
改成:
ftString, ftFixedChar, ftWideString, ftGUID:
if (i = Fields.Count - 1) and (loPartialKey in Options) then
ValStr := Format('''%s*''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])])
else
ValStr := Format('''%s''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])]);
也就是在生成過(guò)濾條件字符串時(shí)把條件的過(guò)濾值中的單引號(hào)全部一個(gè)變兩。
為了確保這樣修改的正確性,我查看了TCustomADODataSet中的對(duì)應(yīng)的LocateRecord過(guò)程(在用TADODataSet中的Lookup字段時(shí)不會(huì)因單引號(hào)出錯(cuò),只在用TCustomClientDataSet時(shí)有這樣的情況),它的處理方法與TCustomClientDataSet稍有不同,它是通過(guò)GetFilterStr函數(shù)來(lái)構(gòu)造過(guò)濾條件的,但在GetFilterStr中,它正確處理了單引號(hào)的問(wèn)題。所以這樣來(lái)看,沒(méi)有在TCustomClientDataSet的LocateRecord中正確處理單引號(hào)的問(wèn)題,確實(shí)是Borland一個(gè)不大不小的疏漏。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注