我支持用 MFC/C++ 编写的旧版应用程序。该应用程序的数据库位于 SQL Server 2000 中。我们最近增加了一些新功能,发现当我们将 SQL Provider 从 SQLOLEDB.1 更改为 SQLNCLI.1 时,一些代码试图通过存储过程从表中检索数据失败。
有问题的表非常简单,是通过以下脚本创建的:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[UAllergenText](
[TableKey] [int] IDENTITY(1,1) NOT NULL,
[GroupKey] [int] NOT NULL,
[Description] [nvarchar](150) NOT NULL,
[LanguageEnum] [int] NOT NULL,
CONSTRAINT [PK_UAllergenText] PRIMARY KEY CLUSTERED
(
[TableKey] ASC) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[UAllergenText] WITH CHECK ADD CONSTRAINT
FK_UAllergenText_UBaseFoodGroupInfo] FOREIGN KEY([GroupKey])
REFERENCES [dbo].[UBaseFoodGroupInfo] ([GroupKey])
GO
ALTER TABLE [dbo].[UAllergenText] CHECK CONSTRAINT
FK_UAllergenText_UBaseFoodGroupInfo]
基本上是四列,其中 TableKey 是一个标识列,其他所有内容都通过以下脚本填充:
INSERT INTO UAllergenText (GroupKey, Description, LanguageEnum)
VALUES (401, 'Egg', 1)
上面还有一长串其他 INSERT INTO 的列表。插入的某些行在其描述中具有特殊字符(如字母上方的重音符号)。我最初认为包含特殊字符是问题的一部分,但如果我完全清除表格,然后只用上面没有特殊字符的单个 INSERT INTO 重新填充它,它仍然会失败。
所以我继续...
然后通过以下代码访问此表中的数据:
std::wstring wSPName = SP_GET_ALLERGEN_DESC;
_variant_t vtEmpty1 (DISP_E_PARAMNOTFOUND, VT_ERROR);
_variant_t vtEmpty2(DISP_E_PARAMNOTFOUND, VT_ERROR);
_CommandPtr pCmd = daxLayer::CDataAccess::GetSPCommand(pConn, wSPName);
pCmd->Parameters->Append(pCmd->CreateParameter("@intGroupKey", adInteger, adParamInput, 0, _variant_t((long)nGroupKey)));
pCmd->Parameters->Append(pCmd->CreateParameter("@intLangaugeEnum", adInteger, adParamInput, 0, _variant_t((int)language)));
_RecordsetPtr pRS = pCmd->Execute(&vtEmpty1, &vtEmpty2, adCmdStoredProc);
//std::wstring wSQL = L"select Description from UAllergenText WHERE GroupKey = 401 AND LanguageEnum = 1";
//_RecordsetPtr pRS = daxLayer::CRecordsetAccess::GetRecordsetPtr(pConn,wSQL);
if (pRS->GetRecordCount() > 0)
{
std::wstring wDescField = L"Description";
daxLayer::CRecordsetAccess::GetField(pRS, wDescField, nameString);
}
else
{
nameString = "";
}
daxLayer 是应用程序正在使用的第三方数据访问库,尽管我们有它的来源(其中一些将在下面看到。) SP__GET_ALLERGEN_DESC 是用于从表中获取数据的存储过程,它是通过创建的这个脚本:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[spRET_AllergenDescription]
-- Add the parameters for the stored procedure here
@intGroupKey int,
@intLanguageEnum int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT Description FROM UAllergenText WHERE GroupKey = @intGroupKey AND LanguageEnum = @intLanguageEnum
END
当 SQL Provider 设置为 SQLNCLI.1 时,应用程序会在以下位置爆炸:
daxLayer::CRecordsetAccess::GetField(pRS, wDescField, nameString);
从上面的代码片段。于是我踏入了GetField,如下所示:
void daxLayer::CRecordsetAccess::GetField(_RecordsetPtr pRS,
const std::wstring wstrFieldName, std::string& sValue, std::string sNullValue)
{
if (pRS == NULL)
{
assert(false);
THROW_API_EXCEPTION(GetExceptionMessageFieldAccess(L"GetField",
wstrFieldName, L"std::string", L"Missing recordset pointer."))
}
else
{
try
{
tagVARIANT tv = pRS->Fields->GetItem(_variant_t(wstrFieldName.c_str()))->Value;
if ((tv.vt == VT_EMPTY) || (tv.vt == VT_NULL))
{
sValue = sNullValue;
}
else if (tv.vt != VT_BSTR)
{
// The type in the database is wrong.
assert(false);
THROW_API_EXCEPTION(GetExceptionMessageFieldAccess(L"GetField",
wstrFieldName, L"std::string", L"Field type is not string"))
}
else
{
_bstr_t bStr = tv ;//static_cast<_bstr_t>(pRS->Fields->GetItem(_variant_t(wstrFieldName.c_str()))->Value);
sValue = bStr;
}
}
catch( _com_error &e )
{
RETHROW_API_EXCEPTION(GetExceptionMessageFieldAccess(L"GetField",
wstrFieldName, L"std::string"), e.Description())
}
catch(...)
{
THROW_API_EXCEPTION(GetExceptionMessageFieldAccess(L"GetField",
wstrFieldName, L"std::string", L"Unknown error"))
}
}
}
这里的罪魁祸首是:
tagVARIANT tv = pRS->Fields->GetItem(_variant_t(wstrFieldName.c_str()))->Value;
进入 Fields->GetItem 将我们带到:
获取项目
inline FieldPtr Fields15::GetItem ( const _variant_t & Index ) {
struct Field * _result = 0;
HRESULT _hr = get_Item(Index, &_result);
if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
return FieldPtr(_result, false);
}
然后将我们带到:
获取值
inline _variant_t Field20::GetValue ( ) {
VARIANT _result;
VariantInit(&_result);
HRESULT _hr = get_Value(&_result);
if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
return _variant_t(_result, false);
}
如果在运行时单步执行时查看 _result,_result 的 BSTR 值是正确的,它的值是表的“描述”字段中的“Egg”。继续遍历所有 COM 释放调用等的跟踪。当我终于回到:
tagVARIANT tv = pRS->Fields->GetItem(_variant_t(wstrFieldName.c_str()))->Value;
跳过它到下一行,现在应该是 BSTR="Egg" 的 tv 的内容是:
tv BSTR = 0x077b0e1c "ᎀݸﻮﻮﻮﻮﻮﻮﻮﻮﻮﻮﻮﻮ㨼㺛帛᠄"
当 GetField 函数尝试将其返回值设置为 tv.BSTR 中的值时
_bstr_t bStr = tv;
sValue = bStr;
不出所料,它会窒息而死。
那么 BSTR 的值发生了什么变化,为什么它只在提供程序设置为 SQLNCLI.1 时才会发生?
最糟糕的是,我在最上面的代码中使用存储过程注释掉了,只是硬编码了存储过程使用的相同 SQL SELECT 语句,发现它工作得很好,返回的值是正确的。
此外,用户可以通过应用程序向表中添加行。如果应用程序在该表中创建一个新行并通过存储过程检索该行,它也可以正常工作,除非您在描述中包含一个特殊字符,在这种情况下它会正确保存该行但以与上面完全相同的方式再次爆炸在检索该行时。
因此,总而言之,如果可以的话,通过 INSERT 脚本放入表中的行在被存储过程访问时总是会炸毁应用程序(无论它们是否包含任何特殊字符)。用户在运行时从应用程序中放入表中的行可以通过存储过程正确检索,除非它们在描述中包含特殊字符,此时它们会破坏应用程序。如果您在运行时使用代码中的 SQL 而不是存储过程访问表中的任何行,则无论描述中是否存在特殊字符,它都可以正常工作。
任何可以对此有所了解的信息都将不胜感激,我提前感谢您。