C++ 存取 SQL Server 是企業級應用開發的核心技能。本文提供從系統環境設定、ADO 智慧指標初始化、連線建立、SQL 執行、結果集處理到完整 CRUD 範例的端到端實戰指南,基於 Visual C++ (MSVC) + ADO (ActiveX Data Objects) 技術棧,適用於 Windows 平台原生 C++ 專案。
| 型別 | 封裝介面 | 用途 |
_ConnectionPtr | Connection | 資料庫連線 |
_RecordsetPtr | Recordset | 結果集/資料列集合 |
_CommandPtr | Command | 參數化命令/預存程序 |
_ParameterPtr | Parameter | 命令參數 |
這些型別源自 _com_ptr_t 模板,自動管理 COM 參考計數 (AddRef/Release),離開作用域自動釋放。
三、建立資料庫連線
#include // _bstr_t, _com_error
#include // CComPtr 替代方案
void ConnectDatabase() {
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); // 初始化 COM 庫
if (FAILED(hr)) return;
_ConnectionPtr pConn = nullptr;
hr = pConn.CreateInstance(__uuidof(Connection));
if (FAILED(hr)) { CoUninitialize(); return; }
try {
// 連線字串 (SQLOLEDB / SQLNCLI11 / MSOLEDBSQL)
_bstr_t strConn = "Provider=SQLOLEDB.1;"
"Server=localhost;" // 或 IP, 執行個體名
"Database=MyDatabase;"
"UID=sa;PWD=YourStrongPassword;";
pConn->Open(strConn, "", "", adConnectUnspecified);
// 連線成功,pConn 可用於後續操作
}
catch (_com_error& e) {
// 錯誤處理
MessageBox(nullptr, (LPCTSTR)e.Description(), L"資料庫連線失敗", MB_ICONERROR);
}
// 使用完畢後:pConn->Close(); pConn = nullptr; CoUninitialize();
}
連線字串關鍵參數
- Provider:
SQLOLEDB (舊)、SQLNCLI11 (Native Client 11)、MSOLEDBSQL (最新 Microsoft OLE DB Driver for SQL Server,**推薦**)。
- Server:
localhost、192.168.1.100、ServerName\InstanceName。
- Trusted_Connection=yes:使用 Windows 整合驗證 (免帳密)。
- Encrypt=yes;TrustServerCertificate=yes:強制加密連線 (Azure SQL/新版驅動程式需求)。
四、執行 SQL 與結果集處理
方法一:Connection::Execute (簡單、無參數化)
_bstr_t sql = "SELECT Id, Name, Price FROM Products WHERE Price > 100";
_RecordsetPtr pRs = pConn->Execute(sql, nullptr, adCmdText);
while (!pRs->adoEOF) {
// 取得欄位值 (三種方式)
// 1. 索引存取 (0-based)
_variant_t vId = pRs->GetCollect(0L);
// 2. 名稱存取
_variant_t vName = pRs->Fields->GetItem("Name")->Value;
// 3. 索引存取 Fields 集合
_variant_t vPrice = pRs->Fields->GetItem(2L)->Value;
// 型別轉換 (_variant_t 多型轉換運算子)
long id = vId.lVal; // VT_I4
CString name = (LPCTSTR)(_bstr_t)vName; // VT_BSTR -> CString
double price = vPrice.dblVal; // VT_R8
// 處理資料...
pRs->MoveNext();
}
pRs->Close();
方法二:Recordset::Open (支援游標/鎖定類型、可更新)
_RecordsetPtr pRs;
pRs.CreateInstance(__uuidof(Recordset));
pRs->Open(_variant_t("SELECT * FROM Products"),
_variant_t((IDispatch*)pConn), // 連線物件
adOpenKeyset, // 游標類型:可捲動、可見他人變更
adLockOptimistic, // 鎖定類型:樂觀鎖定 (更新時檢查)
adCmdText); // 命令類型:SQL 文字
// 更新範例
if (!pRs->adoEOF) {
pRs->Fields->GetItem("Price")->Value = _variant_t(99.9);
pRs->Update(); // 寫回資料庫
}
游標類型選擇指南
| 常數 | 特性 | 適用場景 |
adOpenForwardOnly | 唯讀、僅向前、最快、預設 | 大量資料單向讀取、報表產生 |
adOpenKeyset | 可捲動、可見他人修改、不可見新增/刪除 | 一般互動式應用、需更新 |
adOpenDynamic | 完全動態、可見所有變更、開銷最大 | 高併發即時協作 |
adOpenStatic | 靜態快照、可捲動、不可更新 | 報表、分頁顯示、離線分析 |
五、完整 CRUD 範例 (參數化命令 — 防 SQL Injection)
void InsertProduct(const CString& name, double price, int stock) {
_CommandPtr pCmd;
pCmd.CreateInstance(__uuidof(Command));
pCmd->ActiveConnection = pConn;
pCmd->CommandText = "INSERT INTO Products (Name, Price, Stock) VALUES (?, ?, ?)";
pCmd->CommandType = adCmdText;
// 參數順序對應 ?
pCmd->Parameters->Append(pCmd->CreateParameter("Name", adVarWChar, adParamInput, 100, (LPCTSTR)name));
pCmd->Parameters->Append(pCmd->CreateParameter("Price", adCurrency, adParamInput, 0, price));
pCmd->Parameters->Append(pCmd->CreateParameter("Stock", adInteger, adParamInput, 0, stock));
pCmd->Execute(nullptr, nullptr, adExecuteNoRecords);
}
void UpdateProduct(int id, double newPrice) {
_CommandPtr pCmd;
pCmd.CreateInstance(__uuidof(Command));
pCmd->ActiveConnection = pConn;
pCmd->CommandText = "UPDATE Products SET Price = ? WHERE Id = ?";
pCmd->CommandType = adCmdText;
pCmd->Parameters->Append(pCmd->CreateParameter("Price", adCurrency, adParamInput, 0, newPrice));
pCmd->Parameters->Append(pCmd->CreateParameter("Id", adInteger, adParamInput, 0, id));
pCmd->Execute(nullptr, nullptr, adExecuteNoRecords);
}
void DeleteProduct(int id) {
_CommandPtr pCmd;
pCmd.CreateInstance(__uuidof(Command));
pCmd->ActiveConnection = pConn;
pCmd->CommandText = "DELETE FROM Products WHERE Id = ?";
pCmd->CommandType = adCmdText;
pCmd->Parameters->Append(pCmd->CreateParameter("Id", adInteger, adParamInput, 0, id));
pCmd->Execute(nullptr, nullptr, adExecuteNoRecords);
}
六、錯誤處理與資源管理最佳實踐
統一錯誤處理巨集/函數
#define CHECK_HR(hr, msg) \
if (FAILED(hr)) { \
_com_issue_error(hr); \
}
#define CATCH_COM_ERROR \
catch (_com_error& e) { \
CString errMsg; \
errMsg.Format(L"%s\nHRESULT: 0x%08X\nDescription: %s", \
msg, e.Error(), (LPCTSTR)e.Description()); \
AfxMessageBox(errMsg, MB_ICONERROR); \
} \
catch (...) { \
AfxMessageBox(L"未知嚴重錯誤", MB_ICONERROR); \
}
RAII 封裝連線/記錄集 (現代 C++ 風格)
class AdoConnection {
_ConnectionPtr conn_;
public:
AdoConnection() = default;
~AdoConnection() { if (conn_) { conn_->Close(); conn_ = nullptr; } }
bool Open(const _bstr_t& connStr) {
HRESULT hr = conn_.CreateInstance(__uuidof(Connection));
if (FAILED(hr)) return false;
try { conn_->Open(connStr, "", "", adConnectUnspecified); return true; }
catch (...) { return false; }
}
_ConnectionPtr Get() const { return conn_; }
operator bool() const { return conn_ != nullptr; }
};
class AdoRecordset {
_RecordsetPtr rs_;
public:
AdoRecordset() = default;
~AdoRecordset() { if (rs_) { rs_->Close(); rs_ = nullptr; } }
bool Open(_ConnectionPtr conn, const _bstr_t& sql, long cursor=adOpenForwardOnly, long lock=adLockReadOnly) {
HRESULT hr = rs_.CreateInstance(__uuidof(Recordset));
if (FAILED(hr)) return false;
try { rs_->Open(_variant_t(sql), _variant_t((IDispatch*)conn), cursor, lock, adCmdText); return true; }
catch (...) { return false; }
}
_RecordsetPtr Get() const { return rs_; }
bool Next() { if (rs_ && !rs_->adoEOF) { rs_->MoveNext(); return !rs_->adoEOF; } return false; }
};
七、效能優化與避坑指南
- 連線池:ADO/OLE DB 預設啟用連線池,相同連線字串重複
Open/Close 會複用實體連線。ConnectionString 必須**完全一致** (含空白、大小寫)。
- 參數化命令:務必使用
_CommandPtr + 參數,防止 SQL Injection、提升執行計畫重用率。
- 批次操作:大量寫入用
adExecuteNoRecords、關閉自動認可 (pConn->BeginTrans() / CommitTrans()) 或使用 bcp/SqlBulkCopy (需 .NET/ODBC)。
- 資料型別對應:
_variant_t 轉換需注意 VT_NULL (資料庫 NULL) vs VT_EMPTY,使用 v.vt == VT_NULL 判斷 NULL 值。
- 64 位元專案:確保安裝 64 位元 OLE DB Driver (MSOLEDBSQL),
#import 路徑指向 64 位元 DLL。
八、替代方案現代化選擇