使用 go-mssqldb 的預存程序

驅動程式 go-mssqldb 支援呼叫包含輸入參數、輸出參數及回傳狀態值的儲存程序。 本文將介紹常見的模式。

本文中的大多數程式碼片段都假設一般的 database/sql 設定已就緒,且已將 database/sql 匯入為 sql、有可用的 ctx 值,並且 db 已初始化。 程式碼片段只有在引入其他套件時才會包含匯入區塊,例如 contextfmtlogosgithub.com/microsoft/go-mssqldb。 當程式碼區塊刻意延續同一個範例時,對於該範例前文中已宣告的值,請使用 =;對於獨立程式碼片段中的新宣告,請使用 :=

呼叫預存程序

直接將 ExecContextQueryContext 與程序名稱一起使用:

_, err := db.ExecContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6))

一般程序呼叫時,偏好使用程序名稱加上參數。 只有當你需要將呼叫嵌入較大的 Transact-SQL(T-SQL)批次或使用非EXECUTE ...參數語法時,才使用明確sql.Named字串。

輸出參數

使用 sql.Named 搭配 sql.Out 以接收輸出參數的值:

import (
    "context"
    "database/sql"
    "fmt"
    "log"
)

ctx := context.Background()
var employeeCount int64
_, err := db.ExecContext(ctx, "dbo.GetEmployeeCount",
    sql.Named("count", sql.Out{Dest: &employeeCount}))
if err != nil {
    log.Fatal(err)
}
fmt.Println("Employee count:", employeeCount)

對應的 T-SQL 程序:

CREATE PROCEDURE dbo.GetEmployeeCount
    @count INT OUTPUT
AS
BEGIN
    SELECT @count = COUNT(*) FROM HumanResources.Employee;
END;

輸入/輸出參數

對於同時是輸入與輸出的參數,則設定 In: true 在結構體 sql.Out 上:

var result int64 = 10
_, err := db.ExecContext(ctx, "dbo.DoubleValue",
    sql.Named("value", sql.Out{Dest: &result, In: true}))
if err != nil {
    log.Fatal(err)
}
fmt.Println("Doubled:", result)

返回狀態

mssql.ReturnStatus 來擷取儲存程序的整數回傳值。 此範例使用了 AdventureWorks dbo.uspGetEmployeeManagers 的程序:

import (
    "database/sql"
    "fmt"
    "log"

    mssql "github.com/microsoft/go-mssqldb"
)

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

fmt.Println("Return status:", returnStatus)

ReturnStatus可與 ExecContextQueryContext 搭配使用,但不支援 QueryRowContext

你可以將 ReturnStatus 與程序參數結合。 此範例延續前述範例,重用先前的匯入方式 mssql ,並使用 AdventureWorks dbo.uspGetEmployeeManagers 程序:

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

預存程序傳回的結果集

如果儲存程序回傳結果集,請使用 QueryContext

rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers", sql.Named("BusinessEntityID", 6))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var reportingLevel int
    var employeeID int
    var employeeFirstName string
    var employeeLastName string
    var organizationNode string
    var managerFirstName string
    var managerLastName string
    if err := rows.Scan(&reportingLevel, &employeeID, &employeeFirstName, &employeeLastName, &organizationNode, &managerFirstName, &managerLastName); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Level %d: %d %s %s (manager: %s %s)\n",
        reportingLevel, employeeID, employeeFirstName, employeeLastName, managerFirstName, managerLastName)
    _ = organizationNode
}

對於回傳多個結果集的程序,請使用 rows.NextResultSet()。 欲了解更多資訊,請參閱 查詢與語句

在使用輸出參數或回傳狀態前,先讀取所有列

SQL Server 會在結果集結束後傳送輸出參數並回傳狀態值。 如果你在用完所有列前讀取輸出變數,該值仍可能不完整。

此範例延續前述,並重複使用先前所示的匯入。mssql

它不會列印列資料。 迴圈只會消耗結果集,因此 returnStatus 在查詢結束後才可用。

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var reportingLevel int
    var employeeID int
    var employeeFirstName string
    var employeeLastName string
    var organizationNode string
    var managerFirstName string
    var managerLastName string
    if err := rows.Scan(&reportingLevel, &employeeID, &employeeFirstName, &employeeLastName, &organizationNode, &managerFirstName, &managerLastName); err != nil {
        log.Fatal(err)
    }
    _ = organizationNode
    _ = managerFirstName
    _ = managerLastName
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

fmt.Println("Return status:", returnStatus)

當程序無法回傳資料列時使用 ExecContext 。 如果程序確實回傳了資料列,請等所有資料列和結果集都用完後再讀取輸出參數或回傳狀態。

暫存資料表與儲存程序

當你建立暫存資料表,然後在不同呼叫中查詢時,操作可能會在池子的不同連線上執行。 由於暫存資料表的範圍只限於單一連線,第二次呼叫可能看不到該資料表。

要處理暫存資料表,請使用單一連線方式:

conn, err := db.Conn(ctx)
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

_, err = conn.ExecContext(ctx, "CREATE TABLE #TempItems (Id INT, Name NVARCHAR(50))")
if err != nil {
    log.Fatal(err)
}

_, err = conn.ExecContext(ctx, "INSERT INTO #TempItems VALUES (1, N'Item A')")
if err != nil {
    log.Fatal(err)
}

rows, err := conn.QueryContext(ctx, "SELECT * FROM #TempItems")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

或者,將這些操作放在同一個交易中執行,系統就會自動把它們固定在同一個連線上。

擷取 PRINT 與 RAISERROR 訊息

SQL Server 的 PRINT 陳述式,以及嚴重度為 0-10 的 RAISERROR,會產生不會以 Go 錯誤形式傳回的資訊性訊息。 要擷取這些訊息,請啟用 log 帶有標誌 2 (訊息)的連線參數:

sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&log=2

啟用日誌時,驅動程式會將 PRINT 輸出和低嚴重性 RAISERROR 訊息寫入 Go 的標準 log 套件。 若要程式化擷取,請在開啟連線前設定自訂記錄器:

import (
    "database/sql"
    "log"
    "os"

    mssql "github.com/microsoft/go-mssqldb"
)

// Direct driver messages to a custom logger.
mssql.SetLogger(log.New(os.Stdout, "mssql: ", log.LstdFlags))

db, err := sql.Open("sqlserver",
    "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&log=2")

那麼,任何使用 PRINTRAISERROR(..., 0, 1) 的預存程序,都會將其訊息傳送給你的記錄器。

Note

嚴重度為 11 或以上的 RAISERROR 會產生 Go 錯誤,您可以透過一般的錯誤檢查來處理。 只有 0-10 的嚴重性訊息才需要 log 該參數來捕捉。

欲了解更多關於日誌旗標與程式式日誌擷取的資訊,請參閱 日誌與診斷