Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.