快速入門:使用 C++ 和 ODBC 連線及查詢

適用於:SQL ServerAzure SQL 資料庫

在這個快速入門中,你會在 Windows、Linux 或 macOS 上建立一個 C++ 控制台應用程式。 應用程式透過使用 Microsoft ODBC Driver 18 for SQL Server 連接資料庫AdventureWorksLT,綁定查詢參數,執行查詢,並讀取結果列。

Prerequisites

  • Microsoft ODBC Driver 18 for SQL Server. 安裝 Windows、Linux 或 macOS 的驅動程式。
  • C++17 編譯器以及該平台的 ODBC 開發檔案:
    • 在 Windows 上,請安裝 Visual Studio 2022 或 Visual Studio 2022 Build Tools,並選取 Desktop development with C++ 工作負載。 Windows SDK 提供 ODBC 標頭及 odbc32.lib。
    • 在 Linux 上,安裝 C++ 編譯器和 unixODBC 開發套件作為你的發行版。 封裝提供 ODBC 標頭及 libodbc。
    • 在 macOS 上,安裝 Xcode 命令列工具和 Homebrew 的 unixODBC。
  • Azure SQL Database 或 SQL Server 中的一個資料庫,包含AdventureWorksLT範例資料。 對於 Azure SQL Database,建立單一資料庫時,選擇範例資料來源。 對於 SQL Server,請從 AdventureWorks 範例資料庫還原AdventureWorksLT備份。

確認驅動程式

確認驅動管理員能找到 Microsoft ODBC Driver 18 for SQL Server。

在 PowerShell 中執行以下指令:

Get-OdbcDriver -Name "ODBC Driver 18 for SQL Server"

每個指令都應該列出 ODBC Driver 18 for SQL Server。 如果驅動程式沒有列出,請在繼續前重新安裝。

設定連線

應用程式會從 ODBC_CONNECTION_STRING 環境變數讀取完整的連線字串。 它不會顯示 連接字串,也不會接受它作為命令列參數。

使用適用於你的資料庫和驗證方法的 Driver 18 連線字串。 以下 SQL 認證範例在 Windows、Linux 和 macOS 上運作,前提是資料庫允許 SQL 認證:

Driver={ODBC Driver 18 for SQL Server};Server=tcp:<server>,1433;Database=<database>;UID=<user_id>;PWD=<password>;Encrypt=yes;TrustServerCertificate=no;

關於 Microsoft Entra 認證選項,請參見「使用 Microsoft Entra ID 搭配 ODBC 驅動程式」。 關於所有支援的設定,請參閱 DSN 與 連接字串 關鍵字與屬性。

Microsoft ODBC 驅動程式 18 預設啟用加密功能。 指定 Encrypt=yes 讓應用程式的需求明確。 保持 TrustServerCertificate=no 在生產環境,讓驅動程式驗證伺服器憑證。 憑證必須將伺服器名稱與鏈條與客戶信任的認證機構相匹配。 有關設定指引,請參見憑證驗證失敗。

注意事項

TrustServerCertificate=yes 跳過憑證驗證。 在設定客戶信任的憑證時,只用它來做孤立的本地開發。 不要在生產環境中使用它。

設定環境變數時,不要把 連接字串 加入你的 shell 歷史。

VS 2022 的 Open Developer PowerShell。 執行這些命令,然後在提示字元處貼上連線字串:

$secureConnectionString = Read-Host "ODBC connection string" -AsSecureString
$env:ODBC_CONNECTION_STRING = [System.Net.NetworkCredential]::new(
    "", $secureConnectionString).Password
$secureConnectionString = $null

環境變數會將 連接字串 排除在原始檔案之外,但程序及其子程序可以讀取它。 對於生產應用程式,盡可能使用 Microsoft Entra 認證,並在執行時從安全儲存庫擷取秘密。

建立應用程式

  1. 建立專案目錄並切換至該目錄:

    New-Item -ItemType Directory odbc-quickstart
    Set-Location odbc-quickstart
    

  1. 使用下列程式碼建立名為 odbc-quickstart.cpp 的檔案:

    #ifdef _WIN32
    #include <windows.h>
    #endif
    
    #include <sql.h>
    #include <sqlext.h>
    #include <sqltypes.h>
    
    #include <cstdlib>
    #include <iomanip>
    #include <iostream>
    #include <string>
    #include <string_view>
    
    std::string ReadEnvironmentVariable(const char* name)
    {
    #ifdef _WIN32
        char* value = nullptr;
        std::size_t length = 0;
        if (_dupenv_s(&value, &length, name) != 0 || value == nullptr)
            return {};
    
        std::string result(value);
        std::free(value);
        return result;
    #else
        const char* value = std::getenv(name);
        return value == nullptr ? std::string{} : value;
    #endif
    }
    
    bool PrintDiagnostics(SQLSMALLINT handleType, SQLHANDLE handle)
    {
        bool dataTruncated = false;
        SQLCHAR state[6];
        SQLINTEGER nativeError;
        SQLCHAR message[SQL_MAX_MESSAGE_LENGTH];
        SQLSMALLINT messageLength;
    
        for (SQLSMALLINT record = 1;
             SQL_SUCCEEDED(SQLGetDiagRec(handleType, handle, record, state,
                                         &nativeError, message, sizeof(message),
                                         &messageLength));
             ++record)
        {
            std::cerr << '[' << state << "] (" << nativeError << ") "
                      << message << '\n';
            if (std::string_view(reinterpret_cast<const char*>(state), 5) == "01004")
                dataTruncated = true;
        }
        return dataTruncated;
    }
    
    bool Succeeded(SQLRETURN result, SQLSMALLINT handleType, SQLHANDLE handle,
                   bool* dataTruncated = nullptr)
    {
        if (result == SQL_SUCCESS)
            return true;
    
        const bool diagnosticReportsTruncation = PrintDiagnostics(handleType, handle);
        if (dataTruncated != nullptr)
            *dataTruncated = diagnosticReportsTruncation;
        return result == SQL_SUCCESS_WITH_INFO;
    }
    
    struct OdbcHandles
    {
        SQLHENV environment = SQL_NULL_HENV;
        SQLHDBC connection = SQL_NULL_HDBC;
        SQLHSTMT statement = SQL_NULL_HSTMT;
    
        ~OdbcHandles()
        {
            if (statement != SQL_NULL_HSTMT)
                SQLFreeHandle(SQL_HANDLE_STMT, statement);
            if (connection != SQL_NULL_HDBC)
            {
                SQLDisconnect(connection);
                SQLFreeHandle(SQL_HANDLE_DBC, connection);
            }
            if (environment != SQL_NULL_HENV)
                SQLFreeHandle(SQL_HANDLE_ENV, environment);
        }
    };
    
    int main()
    {
        std::string connectionString =
            ReadEnvironmentVariable("ODBC_CONNECTION_STRING");
        if (connectionString.empty())
        {
            std::cerr << "Set ODBC_CONNECTION_STRING before running.\n";
            return 1;
        }
    
        OdbcHandles handles;
        SQLRETURN result = SQLAllocHandle(
            SQL_HANDLE_ENV, SQL_NULL_HANDLE, &handles.environment);
        if (!Succeeded(result, SQL_HANDLE_ENV, handles.environment))
        {
            std::cerr << "Unable to allocate an ODBC environment handle.\n";
            return 1;
        }
    
        result = SQLSetEnvAttr(
            handles.environment,
            SQL_ATTR_ODBC_VERSION,
            reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3_80),
            0);
        if (!Succeeded(result, SQL_HANDLE_ENV, handles.environment))
            return 1;
    
        result = SQLAllocHandle(
            SQL_HANDLE_DBC, handles.environment, &handles.connection);
        if (!Succeeded(result, SQL_HANDLE_ENV, handles.environment))
            return 1;
    
        result = SQLDriverConnect(
            handles.connection,
            nullptr,
            reinterpret_cast<SQLCHAR*>(connectionString.data()),
            SQL_NTS,
            nullptr,
            0,
            nullptr,
            SQL_DRIVER_NOPROMPT);
        if (!Succeeded(result, SQL_HANDLE_DBC, handles.connection))
            return 1;
    
        result = SQLAllocHandle(
            SQL_HANDLE_STMT, handles.connection, &handles.statement);
        if (!Succeeded(result, SQL_HANDLE_DBC, handles.connection))
            return 1;
    
        SQLINTEGER minimumProductId = 0;
        SQLLEN minimumProductIdLength = 0;
        result = SQLBindParameter(
            handles.statement,
            1,
            SQL_PARAM_INPUT,
            SQL_C_SLONG,
            SQL_INTEGER,
            10,
            0,
            &minimumProductId,
            0,
            &minimumProductIdLength);
        if (!Succeeded(result, SQL_HANDLE_STMT, handles.statement))
            return 1;
    
        SQLCHAR query[] =
            "SELECT TOP (5) ProductID, Name "
            "FROM SalesLT.Product "
            "WHERE ProductID > ? "
            "ORDER BY ProductID;";
        result = SQLExecDirect(handles.statement, query, SQL_NTS);
        if (!Succeeded(result, SQL_HANDLE_STMT, handles.statement))
            return 1;
    
        std::cout << "Product ID  Name\n"
                  << "----------  ----\n";
    
        while ((result = SQLFetch(handles.statement)) != SQL_NO_DATA)
        {
            if (!Succeeded(result, SQL_HANDLE_STMT, handles.statement))
                return 1;
    
            SQLINTEGER productId = 0;
            SQLLEN productIdLength = 0;
            SQLCHAR productName[256] = {};
            SQLLEN productNameLength = 0;
    
            result = SQLGetData(
                handles.statement, 1, SQL_C_SLONG, &productId,
                sizeof(productId), &productIdLength);
            if (!Succeeded(result, SQL_HANDLE_STMT, handles.statement))
                return 1;
    
            bool productNameTruncated = false;
            result = SQLGetData(
                handles.statement, 2, SQL_C_CHAR, productName,
                sizeof(productName), &productNameLength);
            if (!Succeeded(result, SQL_HANDLE_STMT, handles.statement,
                           &productNameTruncated))
                return 1;
    
            std::string productNameText;
            if (productNameLength == SQL_NULL_DATA)
            {
                productNameText = "<NULL>";
            }
            else if (productNameTruncated ||
                     productNameLength > static_cast<SQLLEN>(sizeof(productName) - 1))
            {
                std::cerr << "Product name was truncated; no partial value was printed.\n";
                return 1;
            }
            else if (productNameLength == SQL_NO_TOTAL)
            {
                std::cerr << "Product name length is unknown; no value was printed.\n";
                return 1;
            }
            else if (productNameLength < 0)
            {
                std::cerr << "The driver returned an invalid product-name length.\n";
                return 1;
            }
            else if (productNameLength == 0)
            {
                productNameText = "<empty>";
            }
            else
            {
                productNameText.assign(
                    reinterpret_cast<const char*>(productName),
                    static_cast<std::size_t>(productNameLength));
            }
    
            std::cout << std::left << std::setw(12) << productId
                      << productNameText << '\n';
        }
    
        return 0;
    }
    

該應用程式僅使用標準的 ODBC API,因此包含驅動程式管理員標頭及驅動程式管理員函式庫的連結。 連線字串會在執行階段選擇 Microsoft ODBC Driver 18 for SQL Server。

SQL_DRIVER_NOPROMPT 阻止 SQLDriverConnect 開啟設定對話框。 若 連接字串 不完整,呼叫會回傳錯誤,應用程式會列印所有診斷記錄。 當檢查過的 ODBC 呼叫回傳 SQL_SUCCESS_WITH_INFO時,應用程式也會輸出診斷資訊,表示呼叫成功並發出警告。

此查詢會將 0 綁定為 SQL Server 的 int 參數,讀取 SalesLT.Product 中前五項產品,並使用 SQLGetData 擷取每個產品的 ID 和名稱。 對於產品名稱,應用程式會以 <NULL> 表示 SQL_NULL_DATA,並以 <empty> 表示長度為零的值。 若 SQLGetData 報告 SQLSTATE 01004、長度未知,或長度大於緩衝區,應用程式會回報問題並退出,且不會列印部分名稱。

建置並執行應用程式

  1. 在同一個開發者 PowerShell 視窗中,編譯應用程式:

    cl /std:c++17 /EHsc /W4 odbc-quickstart.cpp /link odbc32.lib
    
  2. 執行應用程式:

    .\odbc-quickstart.exe
    

應用程式的標準輸出為:

Product ID  Name
----------  ----
680         HL Road Frame - Black, 58
706         HL Road Frame - Red, 58
707         Sport-100 Helmet, Red
708         Sport-100 Helmet, Black
709         Mountain Bike Socks, M

完成後清除連接字串。

Remove-Item Env:\ODBC_CONNECTION_STRING