Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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.