使用 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) 的存储过程都会将其消息发送到你的日志记录器。

注释

RAISERROR 的严重性为 11 或更高时,会生成 Go 错误,此错误可通过常规错误检查进行处理。 只有0-10严重度消息需要 log 该参数来捕获。

有关日志标志和程序日志捕获的更多信息,请参见 日志与诊断