Procedimentos armazenados com go-mssqldb

O go-mssqldb driver suporta chamar procedimentos armazenados com parâmetros de entrada, parâmetros de saída e valores de estado de retorno. Este artigo aborda os padrões comuns.

A maioria dos excertos neste artigo assume que a configuração habitual database/sql já está implementada, com database/sql importado como sql, um ctx valor disponível, e db inicializado. Os snippets incluem blocos de importação apenas quando introduzem pacotes adicionais como context, fmt, log, os, ou github.com/microsoft/go-mssqldb. Quando um bloco de código continua intencionalmente o mesmo exemplo, use = para valores que já foram declarados anteriormente nesse exemplo; use := para declarações novas em excertos independentes.

Chamar um procedimento armazenado

Use ExecContext ou QueryContext com o nome do procedimento diretamente:

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

Prefira o nome do procedimento mais os parâmetros para chamadas de procedimentos normais. Use uma string explícita EXECUTE ... apenas quando precisar de incorporar a chamada num lote maior de Transact-SQL (T-SQL) ou use sintaxe que não seja representada por sql.Named parâmetros.

Parâmetros de saída

Use sql.Named com sql.Out para receber valores de parâmetros de saída:

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)

O procedimento T-SQL correspondente:

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

Parâmetros de entrada/saída

Para parâmetros que são tanto entrada como de saída, defina In: true na sql.Out estrutura:

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 da devolução

Use mssql.ReturnStatus para capturar o valor inteiro de retorno de um procedimento armazenado. Este exemplo utiliza o procedimento 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 com ExecContext e QueryContext, mas não com QueryRowContext.

Pode combinar ReturnStatus com os parâmetros do procedimento. Este exemplo continua o exemplo anterior, por isso reutiliza a mssql importação mostrada anteriormente e utiliza o procedimento 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 procedimentos armazenados

Se o procedimento armazenado devolver 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 procedimentos que retornam múltiplos conjuntos de resultados, use rows.NextResultSet(). Para mais informações, consulte Consultas e declarações.

Leia todas as linhas antes de usar parâmetros de saída ou estado de retorno

O SQL Server envia parâmetros de saída e valores de estado de retorno após o término dos conjuntos de resultados. Se leres uma variável de saída antes de consumires todas as linhas, o valor pode continuar incompleto.

Este exemplo continua o anterior e reutiliza a mssql importação mostrada anteriormente.

Não imprime os dados da linha. O ciclo só consome o conjunto de resultados, por isso returnStatus está disponível após o término da 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 quando o procedimento não devolve as linhas. Se o procedimento devolver linhas, aguarde até que todas as linhas e conjuntos de resultados sejam processados antes de ler os parâmetros de saída ou o estado de retorno.

Tabelas temporárias e procedimentos armazenados

Quando crias uma tabela temporária e depois a consultas em chamadas separadas, as operações podem correr em ligações diferentes do pool. Uma vez que as tabelas temporárias estão limitadas a uma única conexão, a segunda chamada pode não ver a tabela.

Para trabalhar com tabelas temporárias, use uma abordagem de ligação ú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()

Em alternativa, envolver as operações numa transação, que as associa automaticamente à mesma ligação.

Capturar mensagens PRINT e RAISERROR

As instruções do SQL Server PRINT e RAISERROR com gravidade 0-10 produzem mensagens informativas que não são devolvidas como erros em Go. Para capturar estas mensagens, ative o log parâmetro de ligação com flag 2 (mensagens):

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

Com o registo ativado, o driver escreve a saída PRINT e as mensagens de baixa gravidade RAISERROR para o pacote log padrão do Go. Para os capturar programaticamente, defina um logger personalizado antes de abrir a ligação:

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

Então, qualquer procedimento armazenado que use PRINT ou RAISERROR(..., 0, 1) envia as suas mensagens para o seu registador.

Note

RAISERROR com gravidade 11 ou superior produz um erro Go que pode ser resolvido com verificação normal de erros. Apenas mensagens de gravidade 0-10 requerem que o log parâmetro seja capturado.

Para mais informações sobre opções de registo e captura programática de registos, consulte Registo e diagnósticos.