Zapytania i instrukcje z go-mssqldb

Sterownik go-mssqldb korzysta ze standardowego database/sql interfejsu do uruchamiania zapytań i wykonywania instrukcji. Ten artykuł omawia typowe wzorce dostępu do danych za pomocą sterownika.

Uruchamianie zapytania SELECT

Użyj QueryContext, aby wykonać zapytanie, które zwraca wiersze:

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)
}

Ważna

Zawsze wywołuj rows.Close() (zazwyczaj z defer) i sprawdź rows.Err() po zakończeniu pętli. Niezamknięcie rzędów może powodować wyciek połączeń z puli. rows.Close() może też zwrócić błąd po stronie serwera, gdy sterownik wyczerpuje pozostałe tokeny, więc nie ignoruj go, gdy zestaw wyników nie jest w pełni wykorzystany.

Jeśli wcześniej zakończysz odczyt, zamknij wiersze jawnie i obsłuż błąd zamknięcia:

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)
}

Przykłady w tym artykule porównane są z przykładową bazą danych AdventureWorks2025 . Przykłady służące do odczytu wysyłają zapytania do wbudowanych obiektów, takich jak Sales.vSalesPerson, Production.Product i Sales.SalesOrderHeader. Przykłady dotyczące zapisu odnoszą się do HumanResources.Department i Production.ProductInventory.

Wykonaj zapytanie o pojedynczy wiersz

Używaj QueryRowContext, gdy spodziewasz się dokładnie jednego wiersza:

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)
}

Wykonaj instrukcję

Użyj ExecContext do INSERT, UPDATE, DELETE i instrukcji 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)

Ważna

Sterownik go-mssqldb nie obsługuje LastInsertId(). Przy wywołaniu zwracany jest błąd. Użyj klauzuli OUTPUT lub oddzielnego zapytania SELECT SCOPE_IDENTITY(), aby pobrać wstawioną wartość identyfikatora.

Jeśli używasz SELECT SCOPE_IDENTITY(), uruchom go w ramach tego samego wsadu lub transakcji co INSERT, aby zakres identyfikatora pozostał w obrębie tego samego połączenia.

Jeśli procedura składowana lub wyzwalacz używa SET NOCOUNT ON, RowsAffected() zwraca 0, ponieważ SQL Server pomija komunikat o liczbie wierszy. Jeśli potrzebujesz rzeczywistej liczby, albo usuń SET NOCOUNT ON z procedury, albo zwróć tę liczbę jawnie za pomocą parametru wyjściowego lub instrukcji SELECT.

Zapytania sparametryzowane

Zawsze używaj parametryzowanych zapytań, aby uniknąć SQL injection. Sterownik obsługuje zarówno parametry pozycyjne, jak i nazwane.

Ważna

Sterownik go-mssqldb używa @p1, @p2 itd. dla parametrów pozycyjnych oraz sql.Named() dla parametrów nazwanych. Składnia symboli zastępczych ?, której używają niektóre inne sterowniki (na przykład go-sql-driver w MySQL), nie działa z nazwą sterownika sqlserver. Jeśli migrujesz z innej bazy danych, zastąp wszystkie symbole zastępcze w stylu ? lub $1 parametrami w stylu @p1 lub parametrami nazwanymi.

Parametry pozycyjne

Użyj symboli zastępczych @p1 i @p2 oraz przekaż wartości w odpowiedniej kolejności:

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

Nazwane parametry

Użyj sql.Named(), aby powiązać wartości z nazwanymi symbolami zastępczymi:

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"))

Wiele zestawów wyników

Użyj elementu rows.NextResultSet(), aby iterować po wielu zestawach wyników zwracanych przez pojedynczą partię poleceń lub procedurę składowaną.

Ważna

Musisz w pełni wyczerpać rows.Next() dla każdego zestawu wyników przed wywołaniem rows.NextResultSet(). Wywołanie NextResultSet() przed Next() zwraca false i po cichu pomija pozostałe wiersze.

Użyj tego wzorca pętli do niezawodnego przetwarzania wszystkich zbiorów wyników:

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

Użyj BeginTx do rozpoczęcia transakcji z określonym poziomem izolacji. Aby uzyskać kompleksowe wskazówki dotyczące transakcji, w tym poziomów izolacji, punktów przywracania, obsługi zakleszczeń oraz wzorców ponawiania prób, zobacz Transakcje.

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)
}

Wprowadź wartości tożsamości

Sterownik go-mssqldb nie obsługuje LastInsertId(). Użyj klauzuli OUTPUT do pobrania wartości tożsamościowej w tym samym zdaniu:

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)

Dla wielu wierszy:

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

Zastosowanie OFFSET i FETCH NEXT do paginacji po stronie serwera. Wymagana jest klauzula ORDER BY :

Paginacja oparta na przesunięciu

Przekaż przesunięcie i rozmiar strony 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()
}

Paginacja oparta na kluczach dla dużych tabel

Paginacja z przesunięciem staje się wolna w przypadku dużych tabel, ponieważ serwer musi pomijać wiersze. Paginacja oparta na kluczu wykorzystuje ostatnio użyty klucz, aby sprawnie pobrać następną stronę:

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()
}

Wskazówka

Paginacja oparta na kluczu jest znacznie szybsza niż OFFSET/FETCH w przypadku odległych stron (strona 1000+), ponieważ wykorzystuje przeszukiwanie indeksu zamiast skanowania i pomijania wierszy.

Wsadowe przetwarzanie wielu instrukcji

Wyślij wiele instrukcji SQL w jednym wywołaniu, aby zmniejszyć liczbę komunikatów zwrotnych w sieci.

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)

Przetwarzaj duże zbiory wyników wydajnie

W przypadku zapytań zwracających miliony wierszy przetwarzaj wyniki strumieniowo. Nie gromadź wszystkich wierszy w pamięci.

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()
}

Uwaga

Otwarty *sql.Rows przypina połączenie z pulą, aż rows.Close() zostanie wywołany. Przy bardzo długotrwałym przetwarzaniu zbiorów wyników rozważ podzielenie pracy na zakresy za pomocą paginacji zestawów kluczy, aby uniknąć utrzymywania połączenia przez kilka minut.

Upsert za pomocą MERGE

SQL Server używa instrukcji MERGE do operacji wstawiania lub aktualizacji (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))

Zapytania przygotowywane

Użyj PrepareContext do stworzenia wielokrotnego użytku przygotowanego oświadczenia. Przygotowane instrukcje mogą poprawić wydajność, gdy to samo zapytanie jest wykonywane wielokrotnie z różnymi parametrami.

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)
}

Anulowanie kontekstu

Wszystkie metody database/sql przyjmują context.Context. Używaj go na przerwy i anulowanie.

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

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

Jeśli upłynie limit czasu kontekstu, sterownik anuluje zapytanie na serwerze i zwróci błąd wywołującemu.