MFC/C++ で記述されたレガシ アプリをサポートしています。アプリのデータベースは SQL Server 2000 にあります。最近、いくつかの新しい機能を追加したところ、SQL プロバイダーを 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]
基本的に 4 つの列で、TableKey は ID 列であり、それ以外はすべて次のスクリプトによって設定されます。
INSERT INTO UAllergenText (GroupKey, Description, LanguageEnum)
VALUES (401, 'Egg', 1)
上記の INSERT INTO に続く他の 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 プロバイダーが 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 にステップインすると、次のようになります。
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);
}
次に、次のようになります。
GetValue
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 値は正しく、その値はテーブルの「説明」フィールドの「卵」です。すべての 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 に設定されている場合にのみ、BSTR の値が発生するのはなぜでしょうか?
なんと、最上位のコードでストアド プロシージャを使用してコメント アウトし、ストアド プロシージャが使用する同じ SQL SELECT ステートメントをハード コーディングしたところ、問題なく動作し、返される値が正しいことがわかりました。
また、ユーザーがアプリケーションを介してテーブルに行を追加することもできます。アプリケーションがそのテーブルに新しい行を作成し、ストアド プロシージャを介してその行を取得する場合、説明に特殊文字を含めない限り正しく機能します。その場合、行は正しく保存されますが、上記とまったく同じ方法で再び爆発します。その行の取得時。
要約すると、可能であれば、INSERT スクリプトを介してテーブルに挿入された行は、ストアド プロシージャによってアクセスされると、常にアプリを爆破します (特殊文字が含まれているかどうかに関係なく)。実行時にユーザーがアプリケーション内からテーブルに挿入した行は、説明に特殊文字が含まれていない限り、ストアド プロシージャを介して正しく取得されます。ストアド プロシージャではなく実行時にコードから SQL を使用してテーブル内の行にアクセスすると、説明に特殊文字があるかどうかに関係なく機能します。
これに光を当てることができれば大歓迎です。事前に感謝します。