Query e istruzioni con go-mssqldb

Il go-mssqldb driver utilizza l'interfaccia standard database/sql per eseguire query ed eseguire istruzioni. Questo articolo tratta i modelli comuni per l'accesso ai dati con il driver.

Eseguire una query SELECT

Usare QueryContext per eseguire una query che restituisce righe:

rows, err := db.QueryContext(ctx,
    "SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
    sql.Named("p1", "Australia"))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var id int
    var name, location string
    if err := rows.Scan(&id, &name, &location); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%d: %s (%s)\n", id, name, location)
}
if err = rows.Err(); err != nil {
    log.Fatal(err)
}

Importante

Chiama rows.Close() sempre (di solito con defer) e controlla rows.Err() dopo il loop. La mancata chiusura delle righe può causare una perdita di connessioni dal pool. rows.Close() Può anche restituire un errore lato server mentre il driver scarica i token rimanenti, quindi non ignorarlo quando il set di risultati non è completamente consumato.

Se smetti di leggere presto, chiudi esplicitamente le righe e gestisci l'errore di chiusura:

rows, err := db.QueryContext(ctx,
    "SELECT TOP (100) ProductID, Name FROM Production.Product ORDER BY ProductID")
if err != nil {
    log.Fatal(err)
}

for rows.Next() {
    var id int
    var name string
    if err := rows.Scan(&id, &name); err != nil {
        _ = rows.Close()
        log.Fatal(err)
    }

    fmt.Printf("%d %s\n", id, name)
    break // Stop early for demonstration.
}

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

Gli esempi in questo articolo vengono confrontati con il database di esempio AdventureWorks2025 . Esempi orientati alla lettura interrogano oggetti integrati come Sales.vSalesPerson, Production.Product, e Sales.SalesOrderHeader. Gli esempi orientati alla scrittura sono destinati a HumanResources.Department e Production.ProductInventory.

Consulta una singola riga

Usa QueryRowContext quando ti aspetti esattamente una riga:

var id int
var name string
err := db.QueryRowContext(ctx,
    "SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE BusinessEntityID = @p1",
    sql.Named("p1", 280)).Scan(&id, &name)
if err == sql.ErrNoRows {
    fmt.Println("No employee found.")
} else if err != nil {
    log.Fatal(err)
} else {
    fmt.Printf("Employee %d: %s\n", id, name)
}

Esegui un'istruzione

Usa ExecContext per INSERT, UPDATE, DELETE, e le istruzioni DDL:

result, err := db.ExecContext(ctx,
    "INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
    sql.Named("p1", "Data Science"),
    sql.Named("p2", "Research and Development"))
if err != nil {
    log.Fatal(err)
}

rowsAffected, _ := result.RowsAffected()
fmt.Printf("Rows affected: %d\n", rowsAffected)

Importante

Il driver go-mssqldb non supporta LastInsertId(). Quando lo si chiama, restituisce un errore. Usa una clausola OUTPUT o una query SELECT SCOPE_IDENTITY() separata per recuperare un valore di identità inserito.

Se si usa SELECT SCOPE_IDENTITY(), eseguirlo nello stesso batch o nella stessa transazione della INSERT, in modo che l'ambito dell'identità rimanga sulla stessa connessione.

Se una stored procedure o un trigger usa SET NOCOUNT ON, RowsAffected() restituisce 0 perché SQL Server sopprime il messaggio di conteggio delle righe. Se hai bisogno del conteggio effettivo, rimuovi SET NOCOUNT ON dalla procedura oppure restituisci il conteggio esplicitamente tramite un parametro o SELECT un'istruzione di output.

Query con parametri

Usa sempre query parametrizzate per evitare l'iniezione SQL. Il driver supporta sia parametri posizionali che parametri denominati.

Importante

Il go-mssqldb driver usa @p1, @p2, e così via per i parametri posizionali e sql.Named() per i parametri nominati. La sintassi del segnaposto ? utilizzata da alcuni altri driver (come go-sql-driver di MySQL) non funziona con il nome driver sqlserver. Se stai migrando da un altro database, sostituisci tutti i segnaposto in stile ? o $1 con parametri in stile @p1 o con nome.

Parametri posizionali

Usa @p1, @p2 segnaposto e passa i valori in ordine:

rows, err := db.QueryContext(ctx,
    "SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
    "Jared", "Australia")

Parametri denominati

Usare sql.Named() per associare valori a segnaposto nominati:

rows, err := db.QueryContext(ctx,
    "SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @location",
    sql.Named("name", "Jared"),
    sql.Named("location", "Australia"))

Più set di risultati

Utilizzare rows.NextResultSet() per scorrere più set di risultati restituiti da un singolo batch o da una procedura archiviata.

Importante

Devi esaurire rows.Next() completamente per ogni set di risultati prima di chiamare rows.NextResultSet(). Chiamare NextResultSet() prima di Next() restituisce false e salta silenziosamente le righe rimanenti.

Usa questo schema a ciclo per elaborare tutti i set di risultati in modo affidabile:

rows, err := db.QueryContext(ctx,
    `SELECT TOP (3) ProductID, Name
     FROM Production.Product
     ORDER BY ProductID;

    SELECT TOP (3) SalesOrderID, CONVERT(NVARCHAR(10), OrderDate, 23) AS OrderDate
     FROM Sales.SalesOrderHeader
     ORDER BY SalesOrderID DESC;`)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

setIndex := 0
for {
    switch setIndex {
    case 0:
        for rows.Next() {
            var productID int
            var productName string
            if err := rows.Scan(&productID, &productName); err != nil {
                log.Fatal(err)
            }
            fmt.Printf("Product %d: %s\n", productID, productName)
        }
    case 1:
        for rows.Next() {
            var salesOrderID int
            var orderDate string
            if err := rows.Scan(&salesOrderID, &orderDate); err != nil {
                log.Fatal(err)
            }
            fmt.Printf("Order %d: %s\n", salesOrderID, orderDate)
        }
    }

    if err := rows.Err(); err != nil {
        log.Fatal(err)
    }
    if !rows.NextResultSet() {
        break
    }
    setIndex++
}

Transactions

Usalo BeginTx per avviare una transazione con un livello di isolamento specifico. Per una guida completa sulle transazioni, inclusi i livelli di isolamento, i punti di salvataggio, la gestione dei deadlock e gli schemi di ritentativo, consulta Transazioni.

tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelSerializable,
})
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()

// Subtract from source location.
_, err = tx.ExecContext(ctx,
    "UPDATE Production.ProductInventory SET Quantity = Quantity - @p1 WHERE ProductID = @p2 AND LocationID = 1",
    sql.Named("p1", 5),
    sql.Named("p2", 1))
if err != nil {
    log.Fatal(err)
}

// Add to destination location.
_, err = tx.ExecContext(ctx,
    "UPDATE Production.ProductInventory SET Quantity = Quantity + @p1 WHERE ProductID = @p2 AND LocationID = 6",
    sql.Named("p1", 5),
    sql.Named("p2", 1))
if err != nil {
    log.Fatal(err)
}

if err = tx.Commit(); err != nil {
    log.Fatal(err)
}

Inserisci valori di identità

Il driver go-mssqldb non supporta LastInsertId(). Usa la clausola OUTPUT per recuperare il valore di identità nella stessa istruzione:

var newID int64
err := db.QueryRowContext(ctx,
    "INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @grp)",
    sql.Named("name", "Data Science"),
    sql.Named("grp", "Research and Development")).Scan(&newID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Inserted department with ID: %d\n", newID)

Per più righe:

rows, err := db.QueryContext(ctx, `
    INSERT INTO HumanResources.Department (Name, GroupName)
    OUTPUT INSERTED.DepartmentID, INSERTED.Name
    VALUES (@n1, @g1), (@n2, @g2)`,
    sql.Named("n1", "Data Science"), sql.Named("g1", "Research and Development"),
    sql.Named("n2", "Cloud Ops"), sql.Named("g2", "Information Technology"))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var id int64
    var name string
    if err := rows.Scan(&id, &name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Inserted: %d - %s\n", id, name)
}

Pagination

Uso OFFSET e FETCH NEXT per la paginazione lato server. È richiesta una ORDER BY clausola:

Paginazione basata su offset

Passa come parametri offset e dimensione della pagina:

func getEmployeesPage(ctx context.Context, db *sql.DB, page, pageSize int) ([]Employee, error) {
    offset := (page - 1) * pageSize
    rows, err := db.QueryContext(ctx, `
        SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
        FROM Sales.vSalesPerson
        ORDER BY BusinessEntityID
        OFFSET @offset ROWS
        FETCH NEXT @pageSize ROWS ONLY`,
        sql.Named("offset", offset),
        sql.Named("pageSize", pageSize))
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var employees []Employee
    for rows.Next() {
        var e Employee
        if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
            return nil, err
        }
        employees = append(employees, e)
    }
    return employees, rows.Err()
}

Paginazione dei keyset per tabelle grandi

La paginazione offset diventa lenta su tabelle grandi perché il server deve saltare le righe. La paginazione del keyset utilizza l'ultima chiave vista per recuperare la pagina successiva in modo efficiente:

func getNextPage(ctx context.Context, db *sql.DB, lastID int, pageSize int) ([]Employee, error) {
    rows, err := db.QueryContext(ctx, `
        SELECT TOP(@pageSize) BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
        FROM Sales.vSalesPerson
        WHERE BusinessEntityID > @lastID
        ORDER BY BusinessEntityID`,
        sql.Named("pageSize", pageSize),
        sql.Named("lastID", lastID))
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var employees []Employee
    for rows.Next() {
        var e Employee
        if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
            return nil, err
        }
        employees = append(employees, e)
    }
    return employees, rows.Err()
}

Tip

La paginazione keyset è significativamente più veloce rispetto a OFFSET/FETCH per le pagine profonde (pagina 1000+) perché utilizza una ricerca nell'indice invece di scansionare e saltare le righe.

Istruzione multipla batch

Invia più istruzioni SQL in una singola chiamata per ridurre i viaggi di andata e ritorno di rete.

rows, err := db.QueryContext(ctx, `
    SELECT COUNT(*) FROM HumanResources.Employee;
    SELECT COUNT(*) FROM Sales.SalesOrderHeader;
    SELECT COUNT(*) FROM Production.Product;`)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

var empCount, orderCount, productCount int

if rows.Next() {
    if err := rows.Scan(&empCount); err != nil {
        log.Fatal(err)
    }
}

if rows.NextResultSet() && rows.Next() {
    if err := rows.Scan(&orderCount); err != nil {
        log.Fatal(err)
    }
}

if rows.NextResultSet() && rows.Next() {
    if err := rows.Scan(&productCount); err != nil {
        log.Fatal(err)
    }
}

if err := rows.Err(); err != nil {
    log.Fatal(err)
}
fmt.Printf("Employees: %d, Orders: %d, Products: %d\n",
    empCount, orderCount, productCount)

Elaborare in modo efficiente grandi set di risultati

Per le query che restituiscono milioni di righe, i risultati del processo si fanno in modo streaming. Non memorizzare tutte le righe in memoria.

func processLargeTable(ctx context.Context, db *sql.DB) error {
    rows, err := db.QueryContext(ctx, "SELECT TransactionID, CONVERT(NVARCHAR(30), TransactionDate, 126) FROM Production.TransactionHistory")
    if err != nil {
        return err
    }
    defer rows.Close()

    var processed int
    for rows.Next() {
        var id int
        var data string
        if err := rows.Scan(&id, &data); err != nil {
            return err
        }

        // Process each row without accumulating.
        if err := handleRow(id, data); err != nil {
            return err
        }

        processed++
        if processed%10000 == 0 {
            log.Printf("Processed %d rows", processed)
        }
    }
    return rows.Err()
}

Attenzione

Un elemento *sql.Rows aperto mantiene occupata una connessione del pool finché non viene chiamato rows.Close(). Per un'elaborazione di set di risultati molto longeva, considera di suddividere il lavoro in intervalli usando la paginazione dei keyset per evitare di mantenere una connessione per minuti.

Upsert con MERGE

SQL Server utilizza questa MERGE istruzione per operazioni di inserimento o aggiornamento (upsert).

_, err := db.ExecContext(ctx, `
    MERGE HumanResources.Department AS target
    USING (SELECT @id AS DepartmentID, @name AS Name, @grp AS GroupName) AS source
    ON target.DepartmentID = source.DepartmentID
    WHEN MATCHED THEN
        UPDATE SET Name = source.Name, GroupName = source.GroupName
    WHEN NOT MATCHED THEN
        INSERT (Name, GroupName)
        VALUES (source.Name, source.GroupName);`,
    sql.Named("id", dept.Id),
    sql.Named("name", dept.Name),
    sql.Named("grp", dept.GroupName))

Istruzioni preparate

Usa PrepareContext per creare una dichiarazione preparata riutilizzabile. Le istruzioni preparate possono migliorare le prestazioni quando la stessa query viene eseguita molte volte con parametri diversi.

stmt, err := db.PrepareContext(ctx,
    "SELECT TOP (1) FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE CountryRegionName = @p1")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

for _, location := range []string{"Australia", "India", "Germany"} {
    var name string
    err := stmt.QueryRowContext(ctx, location).Scan(&name)
    if err != nil {
        log.Println(location, err)
        continue
    }
    fmt.Printf("%s: %s\n", location, name)
}

Cancellazione del contesto

Tutti i database/sql metodi accettano un context.Context. Usalo per timeout e cancellazioni.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")

Se la scadenza contestuale scade, il driver annulla la query sul server e restituisce un errore al chiamante.