Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Ovladač go-mssqldb používá standardní database/sql rozhraní pro spouštění dotazů a provádění příkazů. Tento článek se zabývá běžnými vzorci přístupu k datům pomocí ovladače.
Spuštění dotazu SELECT
Použijte QueryContext k provedení dotazu, který vrací řádky:
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
Vždy volejte rows.Close() (obvykle s defer) a po skončení smyčky zkontrolujte rows.Err(). Neuzavírání řad může dojít k úniku spojení z poolu.
rows.Close() může také vrátit chybu na straně serveru, zatímco ovladač vybíjí zbývající tokeny, takže ji neignorujte, pokud výsledná sada není plně využita.
Pokud přestanete číst dříve, explicitně zavřete řádky a vyřešte chybu uzavření:
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)
}
Příklady v tomto článku jsou porovnány s databází AdventureWorks2025 . Příklady orientované na čtení dotazují vestavěné objekty jako Sales.vSalesPerson, Production.Product, a Sales.SalesOrderHeader. Příklady orientované na zápis jsou zaměřeny na HumanResources.Department a Production.ProductInventory.
Dotazujte se na jeden řádek
Použijte QueryRowContext , když očekáváte přesně jeden řádek:
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)
}
Spustit příkaz
Použijte ExecContext u INSERT, UPDATE, DELETE a příkazů 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)
Important
Ovladač go-mssqldb nepodporuje LastInsertId(). Zavolání vrátí chybu. Použijte klauzuli OUTPUT nebo samostatný SELECT SCOPE_IDENTITY() dotaz k získání vložené hodnoty identity.
Pokud použijete SELECT SCOPE_IDENTITY(), spusťte jej ve stejné dávce nebo transakci jako INSERT, aby kontext identity zůstal ve stejném připojení.
Pokud uložená procedura nebo spouštěč použije SET NOCOUNT ON, RowsAffected() vrátí 0, protože SQL Server potlačí zprávu o počtu řádků. Pokud potřebujete skutečný počet, buď ho z procedury odstraňte SET NOCOUNT ON , nebo ho explicitně vraťte pomocí výstupního parametru či SELECT příkazu.
Parametrizované dotazy
Vždy používejte parametrizované dotazy, abyste se vyhnuli SQL injection. Ovladač podporuje jak polohové, tak pojmenované parametry.
Important
Ovladač go-mssqldb používá @p1, @p2, a tak dále pro polohové parametry a sql.Named() pro pojmenované parametry. Zástupná ? syntaxe, kterou používají některé jiné ovladače (například MySQL go-sql-driver), nefunguje s názvem ovladače sqlserver . Pokud migrujete z jiné databáze, nahraďte všechny zástupné symboly ve stylu ? nebo $1 parametry ve stylu @p1 nebo pojmenovanými parametry.
Poziční parametry
Použijte zástupné symboly @p1, @p2 a předejte hodnoty v daném pořadí:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
Pojmenované parametry
Použijte sql.Named() k přiřazení hodnot k pojmenovaným zástupným prvkům:
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"))
Více výsledkových sad
K iteraci několika sadami výsledků, které vrací jedna dávka nebo uložená procedura, použijte rows.NextResultSet().
Important
Musíte plně vyčerpat rows.Next() pro každou sadu výsledků před vyvoláním rows.NextResultSet(). Volání NextResultSet() před Next() vrátí false a bez upozornění přeskočí zbývající řádky.
Použijte tento vzor smyčky k spolehlivému zpracování všech množin výsledků:
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
Použijte BeginTx k zahájení transakce s konkrétní úrovní izolace. Podrobné pokyny k transakcím, včetně úrovní izolace, bodů obnovení, řešení vzájemného zablokování a strategií opakování, naleznete v části Transakce.
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)
}
Vložte hodnoty identity
Ovladač go-mssqldb nepodporuje LastInsertId(). Použijte klauzuli OUTPUT k získání hodnoty identity ve stejném tvrzení:
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)
Pro více řádků:
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
Použití OFFSET a FETCH NEXT pro stránkování na straně serveru. Je vyžadována ORDER BY klauzule:
Stránkování založené na offsetu
Předejte offset a velikost stránky jako parametry:
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()
}
Stránkování podle klíče pro velké tabulky
Stránkování pomocí offsetu je u velkých tabulek pomalé, protože server musí přeskočit řádky. Paginace klíčových sad používá poslední viděný klíč k efektivnímu načtení další stránky:
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
Stránkování klíčových sad je výrazně rychlejší než OFFSET/FETCH u hlubokých stránek (stránka 1000+), protože používá indexové vyhledávání místo skenování a přeskakování řádků.
Seskupit více příkazů
Pošlete více SQL příkazů v jednom hovoru, abyste snížili počet návratů do sítě.
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)
Efektivně zpracovávejte velké sady výsledků
U dotazů, které vracejí miliony řádků, zpracovávejte výsledky průběžně. Nesbírejte všechny řádky v paměti.
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()
}
Caution
Otevřený *sql.Rows připnutí připne spojení z poolu, dokud rows.Close() není vyvoláno. U velmi dlouhého zpracování sady výsledků zvažte rozdělení práce do rozsahů pomocí keyset pagination, abyste spojení nemuseli držet několik minut.
Upsert pomocí MERGE
SQL Server používá MERGE příkaz pro operace insert-or-update (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))
Připravené příkazy
Použijte PrepareContext k vytvoření opakovaně použitelného připraveného prohlášení. Připravené příkazy mohou zlepšit výkon, když stejný dotaz běží mnohokrát s různými parametry.
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)
}
Zrušení kontextu
Všechny metody database/sql přijímají context.Context. Použijte jej pro časové limity a rušení.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
Pokud termín kontextu vyprší, ovladač dotaz na serveru zruší a vrátí volajícímu chybu.