Dotazy a příkazy s go-mssqldb

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í.

Počet, který uvádí RowsAffected(), závisí na tom, co vytvořilo počty řádků:

  • Uložené procedury: Když každý příkaz běží uvnitř procedury, která používá SET NOCOUNT ON, RowsAffected() vrátí 0, protože není odeslán žádný počet řádků. Pro získání počtu odstraňte SET NOCOUNT ON, nebo vraťte počet pomocí parametru OUTPUT či SELECT příkazu.
  • AFTER triggers:RowsAffected() zahrnuje řádky ovlivněné AFTER triggery, které nepoužívají SET NOCOUNT ON. Jeden řádek UPDATE v tabulce, jehož spouštěč zapíše dva auditní řádky, vrátí 3, ne 1. Přidání SET NOCOUNT ON do těla triggeru vyloučí řádky triggeru a vrátí hodnotu 1. Počet z vnějšího prohlášení je stále uváděn. Neodstraňujte SET NOCOUNT ON z triggeru, abyste opravili počet řádků; jeho odstranění způsobí nadhodnocení počtu.
  • INSTEAD OF spouštěče: Počet zahrnuje příkazy triggeru i původní příkaz, přestože se původní příkaz nespustí. Příkaz UPDATE ovlivňující jediný řádek, jehož spouštěč zapíše dva auditní záznamy a provede aktualizaci, vrátí 4. Pokud spouštěč aktualizaci neprovede, volání stále vrátí 3 a řádky se nezmění, takže nenulový počet nepotvrzuje, že se data změnila. Když potřebujete ověřit výsledek, dotazujte se na postiženou tabulku. Přidání SET NOCOUNT ON do těla spouště vrátí v obou případech 1.
  • Dávky s více příkazy:ExecContext(ctx, "UPDATE ...; UPDATE ...") vrací součet všech započtených příkazů, nikoli počet posledního příkazu. Použijte samostatná volání ExecContext k získání počtu pro každý příkaz.

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.