Procedimientos almacenados con go-mssqldb

El go-mssqldb controlador permite llamar a procedimientos almacenados con parámetros de entrada, parámetros de salida y valores de estado de retorno. Este artículo cubre los patrones más comunes.

La mayoría de los fragmentos de este artículo dan por hecho que la configuración habitual de database/sql ya está preparada, con database/sql importado como sql, un valor de ctx disponible y db inicializado. Los fragmentos incluyen bloques de importación solo cuando introducen paquetes adicionales como context, fmt, log, os, o github.com/microsoft/go-mssqldb. Cuando un bloque de código continúa intencionadamente el mismo ejemplo, use = para valores que ya se declararon antes en ese ejemplo; use := para declaraciones nuevas en fragmentos independientes.

Llamada a un procedimiento almacenado

Usar ExecContext o QueryContext con el nombre del procedimiento directamente:

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

Prefiero el nombre del procedimiento más los parámetros para llamadas ordinarias. Usa una cadena explícita EXECUTE ... solo cuando necesites incrustar la llamada en un lote de Transact-SQL más grande (T-SQL) o usa una sintaxis que no esté representada por sql.Named parámetros.

Parámetros de salida

Usar sql.Named con sql.Out para recibir los valores de los parámetros de salida:

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)

El procedimiento T-SQL correspondiente:

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

Parámetros de entrada y salida

Para los parámetros que sean tanto de entrada como de salida, establezca In: true en la estructura 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)

Estado de devolución

Úsalo mssql.ReturnStatus para capturar el valor entero de retorno de un procedimiento almacenado. Este ejemplo utiliza el procedimiento de 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 funciona con ExecContext y QueryContext, pero no con QueryRowContext.

Puedes combinar ReturnStatus con los parámetros del procedimiento. Este ejemplo continúa el anterior, por lo que reutiliza la mssql importación mostrada antes y utiliza el procedimiento 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()

Conjuntos de resultados a partir de procedimientos almacenados

Si el procedimiento almacenado devuelve conjuntos de resultados, use 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
}

Para procedimientos que devuelven múltiples conjuntos de resultados, utilice rows.NextResultSet(). Para más información, consulte Consultas y declaraciones.

Lee todas las filas antes de usar los parámetros de salida o el estado de retorno

SQL Server envía parámetros de salida y valores de estado de retorno una vez finalizados los conjuntos de resultados. Si lees una variable de salida antes de consumir todas las filas, el valor puede seguir incompleto.

Este ejemplo continúa el anterior y reutiliza la mssql importación mostrada anteriormente.

No imprime los datos de la fila. El bucle solo consume el conjunto de resultados, por lo que returnStatus está disponible después de que termina la consulta.

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)

Use ExecContext cuando el procedimiento no devuelva filas. Si el procedimiento devuelve filas, espera a que se consuman todas las filas y conjuntos de resultados antes de leer los parámetros de salida o el estado de retorno.

Tablas temporales y procedimientos almacenados

Cuando creas una tabla temporal y luego la consultas en llamadas separadas, las operaciones pueden ejecutarse en diferentes conexiones del pool. Como las tablas temporales están limitadas a una sola conexión, la segunda llamada podría no ver la tabla.

Para trabajar con tablas temporales, utiliza un enfoque de conexión única:

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()

Alternativamente, envuelve las operaciones en una transacción, que las vincula automáticamente a la misma conexión.

Capturar mensajes de PRINT y RAISERROR

Las sentencias de SQL Server PRINT y RAISERROR con severidad 0-10 generan mensajes informativos que no se devuelven como errores de Go. Para capturar estos mensajes, activa el log parámetro de conexión con la bandera 2 (mensajes):

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

Con el registro activado, el controlador escribe PRINT mensajes de salida y de baja severidad RAISERROR en el paquete estándar log de Go. Para capturarlos programáticamente, configura un registrador personalizado antes de abrir la conexión:

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")

Luego cualquier procedimiento almacenado que use PRINT o RAISERROR(..., 0, 1) envíe sus mensajes a tu registrador.

Note

RAISERROR con un nivel de gravedad de 11 o más genera un error de Go que puedes gestionar con las comprobaciones de errores habituales. Solo los mensajes de gravedad 0-10 requieren el log parámetro para capturarse.

Para obtener más información sobre los indicadores de registro y la captura programática de registros, consulte Registro y diagnósticos.