news 2026/10/27 11:00:02

VC++ ADO Command对象:参数化查询与存储过程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++ ADO Command对象:参数化查询与存储过程实战

简介:面向VC++数据库开发初学者的一份ADO Command对象实践示例,围绕在MFC/VC++工程中创建_CommandPtr、设置ActiveConnection与CommandText、调用存储过程、绑定参数及执行命令等核心用法展开,适合希望从连接对象进阶到命令对象、理解带参查询与存储过程调用的读者。压缩包共11个文件,约8KB,以cpp与h源码为主,配有vcxproj、sln等Visual Studio工程文件及说明文本,可直接打开解决方案对照编译调试。当前已有525人学习下载,说明其作为入门练习具有一定的参考价值。示例代码把Command对象的属性设置、参数追加、结果集获取和异常处理串成一条完整实践线,便于在VC++项目中模仿改造。读者可据此理解adCmdText与adCmdStoredProc的使用差异,并借助工程文件快速定位入口、头文件声明和事件处理代码,同时积累常见错误排查思路,用于小型数据库访问练习。

1. 一次下单接口从 80 行降到 12 行,靠的是 Command 对象

去年接手一个 VC++ 的老客户端,里面有段下单逻辑:拼接 SQL、判断字段是不是字符串、手动加引号、碰到单引号再转义、最后sprintf拼成一条长串丢给Connection::Execute。一个字段加错位置,整条语句就报语法错,日志里还看不出是哪个参数的问题。后来引入 ADO 的_CommandPtr,把这些散落各处的拼接全部换成?占位符加参数绑定,那段代码从 80 多行压到 12 行,注入漏洞和中文乱码也一起消失了。这个 Ado 实例项目做的就是这件事:用 VC++ 调 ADO,把 Command 对象的参数化查询、存储过程调用、输出参数取值这三条主线跑通,附带一份能编译的最小工程。适合手上还维护着 MFC 或 Win32 客户端、需要跟关系型数据库打交道的开发者,也适合刚接触 COM 智能指针、被_com_error报错绕晕的人。

2. 工程结构与技术选型:ADO 三个核心对象怎么落进 VC++ 工程

ADO 本身是一套 COM 组件,VC++ 用它的方式跟 C# 完全不是一回事:没有 NuGet 包,没有强类型实体,靠的是#import把类型库翻译成.tlh/.tli两个头文件,然后在代码里用_ConnectionPtr、_CommandPtr、_RecordsetPtr这三个智能指针。理解这一点,后面所有的报错和写法就都有了解释。

2.1 目录结构:一个能跑的最小工程长什么样

项目刻意做得很小,为的是把干扰项去掉,让人一眼看清哪部分是 ADO 的,哪部分是业务的。

AdoCommandDemo/ ├── AdoCommandDemo.sln ├── AdoCommandDemo/ │ ├── stdafx.h // 全局只在这里 #import msado15.dll │ ├── stdafx.cpp │ ├── AdoEnv.h // CoInitialize 的 RAII 封装 │ ├── DbConfig.h // 连接串集中管理,不散落在业务代码里 │ ├── OrderRepository.h // 业务数据访问层 │ ├── OrderRepository.cpp // Command 对象的全部用法都在这里 │ ├── main.cpp // 控制台入口,按顺序跑一遍示例 │ └── sql/ │ ├── 01_schema.sql // 建表 │ └── 02_usp_create_order.sql // 带输入、输出参数的存储过程 └── README.md

这种分层在业务项目里可能显得啰嗦,但它把「ADO 怎么用」和「业务怎么跑」隔开了。OrderRepository.cpp是唯一出现_CommandPtr的文件,改数据库、换连接串、换存储过程,都不用动main.cpp。我一般建议刚上手的人先照这个结构抄一遍,跑通之后再往里塞业务,比一上来就丢进现有工程里快得多。

依赖方面只有一项:系统盘里的msado15.dll,Windows 自带,不需要额外安装。所以这个工程拷到任何一台能装 VS 的机器上都能直接编译。

2.2 头文件里的那一行 #import:no_namespace 与 rename 的来由

整套 ADO 用法的基础就在这一行,写错了后面全是编译错误。

// stdafx.h —— 全工程只引入一次,放在预编译头里 #import "C:\Program Files\Common Files\System\ado\msado15.dll" \ no_namespace \ rename("EOF", "adoEOF") \ rename("BOF", "adoBOF")

#import干的事情是读取 DLL 里的类型库,生成msado15.tlh和msado15.tli,编译器再把它当成普通头文件包含进来。三个参数各有各的用处:

  • no_namespace:不加这个,所有 ADO 类型会被塞进一个叫ADODB的命名空间,代码里就得写ADODB::_CommandPtr。业务代码里到处是这种前缀,读起来很难受。
  • rename("EOF", "adoEOF"):这一条纯粹是为了避免名称冲突。stdio.h里有个EOF宏,ADO 的_Recordset也有个EOF属性,两者相撞就是一堆C2059: 语法错误: "常量"。改名之后遍历结果集要写spRs->adoEOF,MoveNext也用adoBOF判断。
  • #import生成的路径默认在Debug/或Release/目录下,clean 之后重新生成一次就行,不用手动管。

提示:如果编译报「无法打开源文件 msado15.tlh」,先确认路径里那个反斜杠是续行符而不是注释,再确认#import只出现了一次。多次引入同一个类型库会产生重定义错误。

2.3 连接与生命周期:CoInitialize、ConnectionTimeout 与智能指针

COM 有个硬规矩:任何 COM 调用之前,当前线程必须先初始化套间。MFC 工程里AfxOleInit()在InitInstance里就干完了,纯 Win32 或控制台程序得自己来。项目里用了个极简的 RAII 包装,保证进入和退出成对:

// AdoEnv.h —— 作用域内自动初始化/反初始化 COM class CAdoEnv { public: CAdoEnv() { m_hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); } ~CAdoEnv() { if (SUCCEEDED(m_hr)) ::CoUninitialize(); } private: HRESULT m_hr; };

用COINIT_APARTMENTTHREADED而不是COINIT_MULTITHREADED,是因为 ADO 的连接对象在套间模型下行为更可预测,跨线程传_ConnectionPtr也更容易出问题——这一点在后面的避坑章节还会提。

连接的打开集中在一个函数里,超时参数一定要设,默认值在断网或者数据库挂掉的时候能让人等到怀疑人生:

_ConnectionPtr OpenDb(const wchar_t* connStr) { _ConnectionPtr spConn; spConn.CreateInstance(__uuidof(Connection)); // 创建 COM 对象 spConn->ConnectionTimeout = 5; // 建连超时,秒 spConn->CommandTimeout = 30; // 单条 Command 执行超时,秒 spConn->Open(_bstr_t(connStr), _bstr_t(L""), _bstr_t(L""), adConnectUnspecified); return spConn; // 返回智能指针,AddRef 自动处理 }

Open的四个参数分别是连接串、用户名、密码、选项。用户名密码在连接串里给了就传空_bstr_t,adConnectUnspecified表示同步打开。连接串本身放在DbConfig.h里,形式大概这样:

// DbConfig.h —— 实际项目里从 ini/注册表读,别硬编码 inline const wchar_t* kConnStr = L"Provider=SQLOLEDB;Data Source=127.0.0.1,1433;" L"Initial Catalog=DemoDB;UID=app_user;PWD=******;";

三个参数值的注意:Provider决定了后面参数占位符的规则(SQLOLEDB只认?,不认@name);Initial Catalog是默认库,省得每条 SQL 都写库名;Data Source后面跟端口要用逗号不是冒号。

3. Command 对象拆解:从 CreateParameter 到 Execute 的完整链路

_CommandPtr的价值全在「参数化」这三个字上。它把 SQL 文本和参数值分成两条通道传给数据库,文本先被解析成执行计划,值再按类型填进去,所以既不会有注入,也不会因为值是数字还是字符串而拼错引号。

3.1 用 ? 占位符写参数化 SQL

先看一段完整的插入,这是项目里出现频率最高的写法:

// OrderRepository.cpp —— 参数化插入,返回受影响行数 long InsertOrder(_ConnectionPtr spConn, const std::wstring& no, long custId, double amount) { _CommandPtr spCmd; spCmd.CreateInstance(__uuidof(Command)); spCmd->ActiveConnection = spConn; // 绑定连接 spCmd->CommandType = adCmdText; // 告诉驱动这是 SQL 文本 spCmd->CommandText = L"INSERT INTO t_order(order_no, cust_id, amount, create_time) " L"VALUES(?, ?, ?, GETDATE())"; // 参数必须严格按 ? 出现的顺序 Append,名字只是给人看的 spCmd->Parameters->Append(spCmd->CreateParameter( L"order_no", adVarWChar, adParamInput, 64, _variant_t(no.c_str()))); spCmd->Parameters->Append(spCmd->CreateParameter( L"cust_id", adInteger, adParamInput, sizeof(long), _variant_t(custId))); spCmd->Parameters->Append(spCmd->CreateParameter( L"amount", adDouble, adParamInput, sizeof(double), _variant_t(amount))); _variant_t vtAffected; spCmd->Execute(&vtAffected, NULL, adExecuteNoRecords); return vtAffected.lVal; // 受影响行数 }

逐段说明:

  • CreateInstance(__uuidof(Command))创建 Command 对象,__uuidof是编译器从#import生成的类型库里取 CLSID,不需要自己写 GUID。
  • ActiveConnection直接赋_ConnectionPtr,靠的是_com_ptr_t的重载运算符自动取接口指针。传NULL也行,那样就得在Execute的第一个参数里传连接。
  • CreateParameter的五个参数是:参数名、数据类型、方向、长度、初值。长度对字符串类型必须给足,adVarWChar的 64 表示最多 64 个宽字符,给 0 会在某些驱动下截断成空串。
  • 第三个参数adExecuteNoRecords很关键。INSERT/UPDATE/DELETE 不返回记录集,带上这个标志能让驱动少建一个空的 Recordset 对象,几千次批量插入下来性能差距肉眼可见。
  • Execute的第一个参数是VARIANT*,接收受影响行数;第二个参数是「替换参数数组」,基本用不上,固定传NULL。

类型选择上有几个高频对照,我一般直接查这张表:

C++ 侧值ADO 类型长度参数说明
longadIntegersizeof(long)int 列
LONGLONG/__int64adBigIntsizeof(LONGLONG)bigint 列、自增主键
doubleadDoublesizeof(double)金额、比例
wchar_t*/_bstr_tadVarWChar字符数上限nvarchar,中文必须用这个
char*adVarChar字节数上限varchar,用错中文会变问号
booladBooleansizeof(VARIANT_BOOL)bit 列

注意:adVarChar配中文字符串是最常见的乱码来源。数据库列是nvarchar就必须用adVarWChar,反过来也成立,两边编码对不上时驱动不会报错,只会默默塞进去一串问号。

3.2 存储过程与输出参数:Parameters 集合的索引取值

存储过程是 Command 对象真正拉开差距的地方,因为只有它能干净地处理输出参数和返回值。

// 调用带输入、输出参数的存储过程,返回新订单 ID LONGLONG CreateOrder(_ConnectionPtr spConn, long custId, const wchar_t* remark) { _CommandPtr spCmd; spCmd.CreateInstance(__uuidof(Command)); spCmd->ActiveConnection = spConn; spCmd->CommandType = adCmdStoredProc; // 关键:声明这是存储过程 spCmd->CommandText = L"usp_create_order"; spCmd->Prepared = VARIANT_TRUE; // 请求驱动缓存执行计划 // 索引 0 固定是返回值,方向必须是 adParamReturnValue spCmd->Parameters->Append(spCmd->CreateParameter( L"RET", adInteger, adParamReturnValue, sizeof(long))); spCmd->Parameters->Append(spCmd->CreateParameter( L"@cust_id", adInteger, adParamInput, sizeof(long), _variant_t(custId))); spCmd->Parameters->Append(spCmd->CreateParameter( L"@remark", adVarWChar, adParamInput, 200, _variant_t(remark))); spCmd->Parameters->Append(spCmd->CreateParameter( L"@order_id", adBigInt, adParamOutput, sizeof(LONGLONG))); spCmd->Execute(NULL, NULL, adExecuteNoRecords); // 用索引取值最稳,名称匹配在不同驱动下行为不一致 long ret = spCmd->Parameters->GetItem(0L)->Value.lVal; LONGLONG orderId = spCmd->Parameters->GetItem(3L)->Value.llVal; if (ret != 0) throw std::runtime_error("usp_create_order failed"); return orderId; }

几个必须说清的点:

  • adCmdStoredProc和adCmdText的差别不只是「告诉驱动这是存储过程」,它还会影响参数怎么派发。设成adCmdText去调存储过程,驱动会把整串当 SQL 文本解析,参数绑定大概率失效。
  • 返回值参数必须第一个Append,方向是adParamReturnValue。这是 ADO 的硬约定,顺序错了取到的就是别的参数值。
  • adParamOutput的参数在Append时不需要给初值,但长度要给。adBigInt给sizeof(LONGLONG),字符串输出参数则要给足字符数,否则会被截断成前几个字符。
  • 取值时用GetItem(索引)而不是GetItem(名称)。SQL Server 的 OLE DB 驱动在adCmdStoredProc下通常支持按名取,但adCmdText场景下参数名是驱动自动生成的Parameter1、Parameter2,按名取会直接抛异常。统一用索引,代码挪到别的驱动上也不会崩。
  • Value返回_variant_t,它继承自tagVARIANT,所以.lVal、.llVal、.dblVal都能直接点出来。类型读错不会报错,只会得到一个垃圾数字,这也是排查时最费劲的一类问题。

3.3 Prepared 与 CommandTimeout 的取舍

Prepared = VARIANT_TRUE会让驱动在第一次执行后缓存执行计划,后续同样的 Command 复用。但它不是无脑开就好:只在「同一个 Command 对象被反复执行、只有参数值变化」的场景下才有收益。如果每次查询都新建一个_CommandPtr,Prepared只会白白多一次往返。我在项目里只在批量循环里开它:

// 批量插入:Command 建一次,参数改值,循环 Execute _CommandPtr spCmd = BuildInsertCommand(spConn); // 内部已设 Prepared = true for (const auto& item : items) { spCmd->Parameters->GetItem(1L)->Value = _variant_t(item.no.c_str()); spCmd->Parameters->GetItem(2L)->Value = _variant_t(item.custId); spCmd->Parameters->GetItem(3L)->Value = _variant_t(item.amount); spCmd->Execute(NULL, NULL, adExecuteNoRecords); }

直接给Parameter->Value赋值比重新Append一遍快一个数量级,因为省掉了参数集合的重建。这里有个前提:参数的类型和长度在第一次Append时就定死了,循环里只能改值不能改形状,否则驱动会抛「参数类型不匹配」。

CommandTimeout是另一个必须显式设的参数。它继承自 Connection,但单条慢查询想单独放宽时,在 Command 上覆盖更合适——比如报表导出给 300 秒,其余保持 30 秒。不设的后果是长事务被默认超时打断,然后事务挂在那里不回滚,连接池慢慢被吃干净。

4. 记录集与事务:Command 返回的 _RecordsetPtr 怎么用

Command 的Execute在查询场景下返回一个_RecordsetPtr,这个对象是有状态的,打开、遍历、关闭必须成对,否则连接会一直被它占着。

4.1 遍历结果集与字段取值

// 按客户查订单,遍历并打印 void PrintOrders(_ConnectionPtr spConn, long custId) { _CommandPtr spCmd; spCmd.CreateInstance(__uuidof(Command)); spCmd->ActiveConnection = spConn; spCmd->CommandType = adCmdText; spCmd->CommandText = L"SELECT order_no, amount FROM t_order " L"WHERE cust_id = ? ORDER BY create_time DESC"; spCmd->Parameters->Append(spCmd->CreateParameter( L"cust_id", adInteger, adParamInput, sizeof(long), _variant_t(custId))); _RecordsetPtr spRs = spCmd->Execute(NULL, NULL, adCmdUnspecified); try { while (!spRs->adoEOF) { _bstr_t no = spRs->Fields->GetItem(L"order_no")->Value; double amt = spRs->Fields->GetItem(L"amount")->Value; printf("%-20s %.2f\n", (const char*)no, amt); spRs->MoveNext(); } spRs->Close(); } catch (_com_error&) { if (spRs != nullptr && spRs->State == adStateOpen) spRs->Close(); throw; // 交给上层统一记日志 } }

Execute的第三个参数这里给adCmdUnspecified,意思是「按 Command 对象上已经设好的CommandType走」。前面设过adCmdText就够了,重复指定反而容易在驱动之间产生分歧。Fields->GetItem按列名取字段,返回_FieldPtr,它的Value是_variant_t,赋给_bstr_t或double时会自动转换。取不到列名会抛_com_error,错误码是0x8002000B(找不到成员),看到这个先去核对 SELECT 列表。

catch块里的Close是血泪经验。异常抛出后栈会展开,但_RecordsetPtr的析构只负责 Release,不保证把游标关掉,服务端那边的连接就一直挂着。显式关一次,再往上抛。

4.2 事务:把多条 Command 绑在同一条连接上

ADO 的事务是连接级的,BeginTrans/CommitTrans/RollbackTrans三个方法挂在_Connection上。这带来一个容易被忽略的约束:参与同一事务的所有 Command,ActiveConnection必须指向同一个连接对象。

// 转账示例:两条 Command,一个事务 void Transfer(_ConnectionPtr spConn, long from, long to, double money) { spConn->IsolationLevel = adXactReadCommitted; spConn->BeginTrans(); try { _CommandPtr spOut = MakeCommand(spConn, L"UPDATE t_account SET balance = balance - ? WHERE id = ?"); spOut->Parameters->GetItem(1L)->Value = _variant_t(money); spOut->Parameters->GetItem(2L)->Value = _variant_t(from); _variant_t vAffected; spOut->Execute(&vAffected, NULL, adExecuteNoRecords); if (vAffected.lVal != 1) throw std::runtime_error("source account not found"); _CommandPtr spIn = MakeCommand(spConn, L"UPDATE t_account SET balance = balance + ? WHERE id = ?"); spIn->Parameters->GetItem(1L)->Value = _variant_t(money); spIn->Parameters->GetItem(2L)->Value = _variant_t(to); spIn->Execute(NULL, NULL, adExecuteNoRecords); spConn->CommitTrans(); } catch (...) { spConn->RollbackTrans(); // 任何异常都回滚 throw; } }

IsolationLevel必须在BeginTrans之前设,之后改无效。RollbackTrans放在catch里而不是析构里,是因为回滚本身也可能失败(连接已断),失败了要往上抛让调用方知道数据没落地。判断受影响行数是事务里最实用的一招,vAffected.lVal != 1立刻能发现「账户不存在」这类静默失败,不然事务照样提交,账就错了。

4.3 客户端游标与批量更新

需要「读一批、改一批、最后一次性提交」的场景,用服务端游标会一行一个往返,慢得离谱。切到客户端游标加批量乐观锁,把往返压成一次:

_RecordsetPtr spRs(__uuidof(Recordset)); spRs->CursorLocation = adUseClient; // 游标拉回本地 spRs->Open(_variant_t(L"SELECT order_id, amount FROM t_order WHERE cust_id = 1"), _variant_t((IDispatch*)spConn), // 传连接对象本身 adOpenStatic, adLockBatchOptimistic, adCmdText); while (!spRs->adoEOF) { spRs->Fields->GetItem(L"amount")->Value = _variant_t(1.05 * (double)spRs->Fields->GetItem(L"amount")->Value); spRs->MoveNext(); } spRs->UpdateBatch(adAffectAll); // 一次性提交所有改动 spRs->Close();

adLockBatchOptimistic是批量更新的前提,配adLockPessimistic会在读取时就锁行,循环里改到一半被别人卡住。UpdateBatch内部走的是批量协议,几百行的更新耗时通常只有逐行Update的十分之一。代价是冲突检测推迟到提交那一刻,出现并发修改时会抛_com_error,得自己决定是重试还是放弃。

5. 避坑与排查:Command 对象最容易翻车的五个地方

这一节按「现象 → 原因 → 解决」来写,都是实际调试出来的。

现象:编译期报C2059: 语法错误: "常量",位置在 msado15.tlh 里。原因是EOF与stdio.h的宏冲突。解决:#import那行必须带rename("EOF", "adoEOF")和rename("BOF", "adoBOF"),然后把代码里所有spRs->EOF改成spRs->adoEOF。

现象:运行时抛_com_error,错误码0x800401F0,描述是「尚未调用 CoInitialize」。原因是在工作线程里直接用 ADO 了。CoInitialize是线程级的,主线程初始化过不代表子线程也初始化了。解决:把所有碰 ADO 的代码收进一个带 RAII 的入口,或者在线程函数开头CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)、结尾CoUninitialize()。顺带提醒,跨线程传_ConnectionPtr在套间模型下是不安全的,正确做法是每个线程自己开一条连接。

现象:Execute抛「参数不足」或「参数类型不匹配」,SQL 里明明写了三个?。原因有两类。一是Append的顺序和?的顺序对不上,或者漏了返回值参数。二是误以为SQLOLEDB支持@name形式的命名参数——它只认按位置的?,写成@cust_id会被当成字面量,参数集合里的值永远填不进去。解决:SQL 文本一律用?,参数严格按顺序Append,取值一律用索引不按名。

现象:输出参数取出来是空串或者 0,存储过程本身跑着没问题。原因是CreateParameter的方向参数写成了adParamInput,或者长度给了 0。字符串输出参数的长度必须给足字符数,adBigInt必须给sizeof(LONGLONG),给sizeof(long)会读到高位截断的值。解决:先把参数方向改成adParamOutput打印一遍Parameters->Count,确认个数和顺序,再逐个核对长度;取值时用.llVal而不是.lVal。

现象:跑一段时间后连接数暴涨,或者程序退出时卡在CoUninitialize。原因是循环里反复CreateInstance却没收尾,_Recordset没Close,_Command一直持有连接引用。智能指针只管引用计数,不管游标状态。解决:把 Recordset 和 Command 都放进作用域,Close放在catch里也放一份;长时间运行的服务用一个连接池管理类统一持有,别让业务代码自己CreateInstance。

提示:_com_error::Description()返回的是驱动的原始报错,比ErrorMessage()有用得多。日志里两个都打,能省掉很多猜的时间。

6. 二次开发:把散落的 Command 调用收敛成一个 CAdoCommand 类

这个实例跑通之后,我最先动的地方不是加功能,而是把OrderRepository.cpp里重复了十几遍的「CreateInstance → 设 ActiveConnection → 设 CommandType → Append 参数」抽成一个类。原因很简单:这些样板代码一多,谁都会在某处漏设一个CommandType,然后花半小时查为什么参数没生效。

一个够用的封装大概长这样,重点在析构里自动Close和统一的错误翻译:

// CAdoCommand.h —— 链式构造 + RAII 释放 class CAdoCommand { public: explicit CAdoCommand(_ConnectionPtr spConn, long type = adCmdText) { m_spCmd.CreateInstance(__uuidof(Command)); m_spCmd->ActiveConnection = spConn; m_spCmd->CommandType = type; m_spCmd->CommandTimeout = 30; } CAdoCommand& Sql(const wchar_t* text) { m_spCmd->CommandText = text; return *this; } CAdoCommand& In(const wchar_t* name, DataType type, int size, const _variant_t& v) { m_spCmd->Parameters->Append( m_spCmd->CreateParameter(name, type, adParamInput, size, v)); return *this; } CAdoCommand& Out(const wchar_t* name, DataType type, int size) { m_spCmd->Parameters->Append( m_spCmd->CreateParameter(name, type, adParamOutput, size)); return *this; } _variant_t Param(long idx) const { return m_spCmd->Parameters->GetItem(idx)->Value; } long ExecuteNoRecord() { _variant_t n; m_spCmd->Execute(&n, NULL, adExecuteNoRecords); return n.lVal; } private: _CommandPtr m_spCmd; // 析构自动 Release,无需手动清理 };

调用侧就变成一行链式:

long affected = CAdoCommand(spConn) .Sql(L"UPDATE t_order SET status = ? WHERE order_id = ?") .In(L"status", adInteger, sizeof(long), _variant_t(2L)) .In(L"order_id", adBigInt, sizeof(LONGLONG), _variant_t(orderId)) .ExecuteNoRecord();

改造的时候建议按这个顺序验证,一步一停,别一口气全换完:

步骤验证方法通过标准
1. 抽公共构造用封装后的类重跑一条最简单的 UPDATE受影响行数与手写版一致
2. 换存储过程调用同一个存储过程,新旧两版各跑一次输出参数、返回值完全相同
3. 加错误翻译故意传一个不存在的表名日志里能看到驱动原始 Description
4. 压测释放循环调一万次,观察连接数与句柄数曲线平稳,退出不卡

最容易被忽略的是第 4 步。封装之后Execute调用变短了,反而更容易在循环里随手构造临时对象,句柄数悄悄爬升。我一般在循环外面套一个GetGuiResources或者任务管理器的句柄列看着跑,涨幅超过阈值就说明有对象没放掉。

还有一点,别在封装层里做「自动重连」。我见过一段代码,_com_error一出现就重新Open,结果在一个已经失败的事务里反复重连,脏数据写了两遍。连接断了就让异常往上抛,重试策略放在业务层,那里才知道幂等边界在哪。

从那以后我每次接这种 ADO 老项目,都强制先把参数收集和错误翻译走一遍最小验证,哪怕只是改一个字段类型,也要拿真实数据库跑一遍再提交。跳过这一步省下的十分钟,通常会在上线后还成半天。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 10:59:44

SolidWorks螺旋桨流体仿真:旋转域设置与推力扭矩计算

简介:面向机械工程、航空航天与船舶推进方向的学习者及工程技术人员,这份螺旋桨流体仿真资料包围绕CFD分析与SolidWorks Flow Simulation展开,可用于理解螺旋桨推力、扭矩、阻力等性能指标的仿真流程,并为叶片几何调整与方案对比提…

作者头像 李华
网站建设 2026/10/27 10:58:43

AI Agent开发:MCP与Skill架构解析与实践

1. AI Agent开发中的核心组件:MCP与Skill的本质解析 在AI Agent开发领域,MCP(Master Control Program)和Skill是两种最核心的架构组件。我见过太多开发者在这两个概念上栽跟头——有人把Skill写成微型MCP,也有人试图用…

作者头像 李华
网站建设 2026/10/27 10:55:40

Agentic AI如何革新API测试:原理、实践与优化

1. Agentic AI在API测试中的核心价值 Agentic AI(自主智能体)正在彻底改变传统API测试的工作方式。这种具备自主决策能力的AI系统,不再局限于执行预设脚本,而是能够理解API规范、分析业务场景并动态生成测试策略。我们团队在实际项…

作者头像 李华
网站建设 2026/10/27 10:54:11

Windows下Mosquitto搭建MQTT学习环境全指南

1. 为什么选择Mosquitto搭建MQTT学习环境 MQTT协议作为物联网领域最主流的通信协议之一,其轻量级、低功耗和发布/订阅模式的特点,使其在智能家居、工业物联网等场景中广泛应用。而Mosquitto作为Eclipse基金会维护的开源MQTT broker,凭借其跨平…

作者头像 李华
网站建设 2026/10/27 10:53:37

物流运单状态机设计:实时协同与事件驱动架构

简介:本资源是一套完整的物流管理系统前后端实战项目,面向Java/Web开发初学者与中级工程师,聚焦企业级物流业务场景下的系统设计与实现。资源涵盖客户下单查询的前台交互界面与订单处理、仓储调度、异常管理等后台核心功能,深度融…

作者头像 李华
网站建设 2026/10/27 10:53:05

分布式账号矩阵优化:提升Twitter行为信号触达效率

1. 分布式账号矩阵的行为信号放大原理在社交媒体运营领域,多账号矩阵管理是个经久不衰的话题。最近我接手了一个跨境电商客户的Twitter(X)账号矩阵优化项目,发现传统的"多开定时发布"模式已经难以适应当前平台算法。通过引入分布式系统设计理念…

作者头像 李华