Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Драйвер go-mssqldb использует стандартный интерфейс database/sql для выполнения запросов и инструкций. В этой статье рассматриваются распространённые паттерны доступа к данным с помощью драйвера.
Выполнение запроса SELECT
Используйте QueryContext для выполнения запроса, который возвращает строки:
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() (обычно с помощью defer) и проверяйте rows.Err() после цикла. Если не закрывать строки, это может привести к утечке соединений из пула.
rows.Close() Также может вернуть серверную ошибку, пока драйвер расходует оставшиеся токены, так что не игнорируйте её, если набор результатов не полностью используется.
Если вы прекращаете чтение раньше времени, явно закройте набор строк и обработайте ошибку, возникающую при закрытии:
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)
}
Примеры в этой статье выполняются в образце базы данных AdventureWorks2025. Примеры, ориентированные на чтение, запрашивают встроенные объекты, такие как Sales.vSalesPerson, Production.Productи Sales.SalesOrderHeader. Примеры, ориентированные на запись, предназначены для HumanResources.Department и Production.ProductInventory.
Запрос одной строки
Используйте QueryRowContext, когда ожидаете ровно одну строку:
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)
}
Выполнить инструкцию
Используйте ExecContext для INSERT, UPDATE, DELETE и операторов 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
go-mssqldb Драйвер не поддерживает LastInsertId(). При его вызове возникает ошибка. Используйте OUTPUT клаузу или отдельный SELECT SCOPE_IDENTITY() запрос для получения вставленного значения идентичности.
Если вы используете SELECT SCOPE_IDENTITY(), выполняйте его в том же пакете или транзакции, что и INSERT, чтобы область действия идентификатора оставалась в рамках того же соединения.
Если сохранённая процедура или триггер использует SET NOCOUNT ON, RowsAffected() возвращает 0, потому что SQL Server подавляет сообщение о подсчёте строк. Если вам нужно фактическое количество, либо удалите SET NOCOUNT ON из процедуры, либо явно верните это количество с помощью выходного параметра или оператора SELECT.
Параметризованные запросы
Всегда используйте параметризованные запросы, чтобы избежать SQL-инъекции. Драйвер поддерживает как позиционные, так и именованные параметры.
Important
Драйвер go-mssqldb использует @p1, @p2и так далее для позиционных параметров и sql.Named() именованных параметров. Синтаксис заполнителей ?, который используют некоторые другие драйверы (например, MySQL go-sql-driver), не работает с драйвером sqlserver. Если вы переходите с другой базы данных, замените все заполнители в стиле ? или $1 на заполнители в стиле @p1 или именованные параметры.
Позиционные параметры
Используйте плейсхолдеры @p1, @p2 и передавайте значения по порядку:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
Именованные параметры
Используйте sql.Named() для привязки значений к именованным заполнителям:
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"))
Множество результирующих наборов
Используйте rows.NextResultSet(), чтобы перебирать несколько наборов результатов, которые возвращает один пакет или хранимая процедура.
Important
Вы должны полностью исчерпать rows.Next() для каждого набора результатов перед вызовом rows.NextResultSet(). Вызов NextResultSet() перед Next() возвращает false и молча пропускает оставшиеся строки.
Используйте этот шаблон цикла для надёжной обработки всех наборов результатов:
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
Используйте BeginTx для начала транзакции с определённым уровнем изоляции. Для полных рекомендаций по транзакциям, включая уровни изоляции, точки сохранения, обработку тупиков и шаблоны повторных попыток, см. раздел Транзакции.
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)
}
Получите вставленные значения идентичности
go-mssqldb Драйвер не поддерживает LastInsertId(). Используйте предложение OUTPUT , чтобы получить тождественное значение из того же утверждения:
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)
Для нескольких строк:
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
Используйте OFFSET и FETCH NEXT для пагинации на стороне сервера.
ORDER BY Требуется оговорка:
Пагинирование на основе смещения
Передайте смещение и размер страницы в качестве параметров:
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()
}
Пагинация по ключу для больших таблиц
Пагинация со смещением работает медленно на больших таблицах данных, поскольку серверу приходится пропускать строки. Пагинация наборов ключей использует последний увиденный ключ для эффективного получения следующей страницы:
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
Пагинация ключевых наборов значительно быстрее, чем OFFSET/FETCH для глубоких страниц (страница 1000+), поскольку использует индексный поиск вместо сканирования и пропуска строк.
Сгруппировать несколько операторов
Отправляйте несколько SQL-инструкций в одном вызове, чтобы сократить количество сетевых обменов.
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)
Эффективно обрабатывайте большие наборы результатов
Для запросов, возвращающих миллионы строк, обрабатывайте результаты в потоковом режиме. Не накапливайте все строки в памяти.
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()
}
Предостережение
Открытая *sql.Rows удерживает соединение из пула до вызова rows.Close(). Для очень длительной обработки наборов результатов рассмотрите возможность разбить работу на диапазоны с помощью пагинации ключевых наборов, чтобы избежать задержки соединения в течение нескольких минут.
Обновление или вставка с MERGE
SQL Server использует оператор MERGE для операций вставки или обновления данных (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))
Подготовленные выражения
Используйте PrepareContext, чтобы создать подготовленный запрос, который можно использовать повторно. Подготовленные выражения могут повысить производительность, если один и тот же запрос многократно выполняется с разными параметрами.
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)
}
Отмена контекста
Все методы database/sql принимают context.Context. Используйте его для тайм-аутов и отмены.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
Если срок действия контекста истекает, драйвер отменяет запрос на сервере и возвращает ошибку вызывающему.