Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Der Treiber go-mssqldb verwendet die Standardschnittstelle database/sql zum Ausführen von Abfragen und zur Ausführung von Anweisungen. Dieser Artikel behandelt gängige Muster für den Datenzugriff mit dem Treiber.
Ausführen einer SELECT-Abfrage
Verwenden Sie QueryContext, um eine Abfrage auszuführen, die Zeilen zurückgibt:
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)
}
Important
rows.Close() immer aufrufen (typischerweise mit defer) und rows.Err() nach der Schleife überprüfen. Wenn Zeilen nicht geschlossen werden, kann dies zum Verlust von Verbindungen aus dem Verbindungspool führen.
rows.Close() kann auch einen serverseitigen Fehler zurückgeben, während der Treiber verbleibende Tokens verbraucht, also ignorieren Sie ihn nicht, wenn die Ergebnismenge nicht vollständig verbraucht ist.
Wenn du früh aufhörst zu lesen, schließe die Zeilen explizit und behandle den Schlussfehler:
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)
}
Beispiele in diesem Artikel laufen gegen die AdventureWorks2025-Beispieldatenbank . Leseorientierte Beispiele fragen eingebaute Objekte wie Sales.vSalesPerson, Production.Product, und Sales.SalesOrderHeaderab. Schreiborientierte Beispiele richten sich an HumanResources.Department und Production.ProductInventory.
Eine einzelne Zeile abfragen
Verwenden Sie QueryRowContext, wenn Sie genau eine Zeile erwarten:
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)
}
Führe eine Anweisung aus
Verwenden Sie ExecContext für INSERT, UPDATE, DELETE und DDL-Anweisungen:
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)
Important
Der Treiber go-mssqldb unterstützt LastInsertId()nicht . Das Aufrufen liefert einen Fehler. Verwenden Sie eine OUTPUT Klausel oder eine separate SELECT SCOPE_IDENTITY() Abfrage, um einen eingefügten Identitätswert abzurufen.
Wenn Sie SELECT SCOPE_IDENTITY() verwenden, führen Sie es im selben Batch oder in derselben Transaktion wie INSERT aus, damit der Identitätsbereich auf derselben Verbindung bleibt.
Wenn eine gespeicherte Prozedur oder ein Trigger SET NOCOUNT ON verwendet, gibt RowsAffected() 0 zurück, weil SQL Server die Zeilenanzahlmeldung unterdrückt. Wenn Sie die tatsächliche Anzahl benötigen, entfernen Sie entweder SET NOCOUNT ON aus der Prozedur oder geben Sie die Anzahl explizit über einen Ausgabeparameter oder eine SELECT-Anweisung zurück.
Parametrisierte Abfragen
Verwenden Sie immer parametrisierte Abfragen, um SQL-Injection zu vermeiden. Der Treiber unterstützt sowohl positionsbezogene als auch benannte Parameter.
Important
Der Treiber go-mssqldb verwendet @p1, @p2, und so weiter für Positionsparameter und sql.Named() für benannte Parameter. Die ? Platzhaltersyntax, die einige andere Treiber verwenden (wie die go-sql-drivervon MySQL), funktioniert nicht mit dem Treibernamen sqlserver . Wenn Sie von einer anderen Datenbank migrieren, ersetzen Sie alle ? oder $1-Stil Platzhalter durch @p1-Stil oder benannte Parameter.
Positionsparameter
Verwenden Sie @p1, @p2 Platzhalter und Passwerte in der Reihenfolge:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
Benannte Parameter
Verwenden Sie sql.Named(), um Werte an benannte Platzhalter zu binden:
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"))
Mehrere Ergebnismengen
rows.NextResultSet() verwenden, um mehrere Ergebnissätze zu durchlaufen, die von einem einzelnen Batch oder einer Stored Procedure zurückgegeben werden.
Important
Du musst rows.Next() für jede Ergebnismenge vollständig verarbeiten, bevor du rows.NextResultSet() aufrufst. Der Aufruf von NextResultSet() vor Next() gibt false zurück und überspringt die verbleibenden Zeilen stillschweigend.
Verwenden Sie dieses Schleifenmuster, um alle Ergebnismengen zuverlässig zu verarbeiten:
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++
}
Transaktionen
Verwenden Sie BeginTx, um eine Transaktion mit einer bestimmten Isolationsstufe zu starten. Für umfassende Transaktionshinweise, einschließlich Isolationsstufen, Speicherpunkte, Deadlock-Handling und Wiederholungsmustern, siehe Transaktionen.
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)
}
Eingefügte Identitätswerte abrufen
Der Treiber go-mssqldb unterstützt LastInsertId()nicht . Verwenden Sie die Klausel OUTPUT, um den Identitätswert in derselben Anweisung abzurufen:
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)
Für mehrere Reihen:
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)
}
Paginierung
Verwendung OFFSET und FETCH NEXT für serverseitige Paginierung. Eine Klausel ORDER BY ist erforderlich:
Offset-basierte Paginierung
Übergeben Sie den Offset und die Seitengröße als Parameter:
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()
}
Keyset-Paginierung für große Tabellen
Die Offset-Paginierung wird bei großen Tabellen langsam, weil der Server Zeilen überspringen muss. Die Keyset-Paginierung verwendet die zuletzt gesehene Taste, um die nächste Seite effizient abzurufen:
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
Keyset-Paginierung ist deutlich schneller als OFFSET/FETCH bei tiefen Seiten (Seite 1000+), da sie eine Indexsuche anstelle von Scannen und Überspringen von Zeilen verwendet.
Mehrere Anweisungen stapelweise verarbeiten
Sende mehrere SQL-Anweisungen in einem einzigen Aufruf, um Netzwerk-Roundtrips zu reduzieren.
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)
Verarbeiten Sie große Ergebnismengen effizient
Bei Abfragen, die Millionen von Zeilen zurückgeben, sollten Ergebnisse im Streamingverfahren verarbeitet werden. Sammle nicht alle Zeilen im Gedächtnis.
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()
}
Achtung
Ein offenes *sql.Rows Signal verbindet eine Verbindung vom Pool, bis rows.Close() aufgerufen wird. Für eine sehr langlaufende Verarbeitung von Ergebnismengen sollten Sie die Arbeit in Bereiche mit Keyset-Pagination unterteilen, um eine Verbindung nicht minutenlang zu halten.
Upsert mit MERGE
SQL Server verwendet die MERGE Anweisung für Insert-or-Update-(Upsert)-Operationen.
_, 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))
Vorbereitete Anweisungen
Verwenden Sie PrepareContext , um eine wiederverwendbare vorbereitete Erklärung zu erstellen. Prepared Statements können die Leistung verbessern, wenn dieselbe Abfrage mehrfach mit unterschiedlichen Parametern ausgeführt wird.
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)
}
Kontextaufhebung
Alle database/sql Methoden akzeptieren ein context.Context. Nutze es für Auszeiten und Absagen.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
Wenn die Kontextfrist abläuft, bricht der Treiber die Abfrage auf dem Server ab und gibt dem Aufrufer einen Fehler zurück.