Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
go-mssqldb Driver menggunakan antarmuka standar database/sql untuk menjalankan kueri dan menjalankan pernyataan. Artikel ini membahas pola umum dalam akses data dengan driver.
Menjalankan kueri SELECT
Gunakan QueryContext untuk menjalankan kueri yang mengembalikan baris:
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
Selalu panggil rows.Close() (biasanya dengan defer) dan periksa rows.Err() setelah loop. Gagal menutup baris dapat membocorkan koneksi dari kolam.
rows.Close() juga dapat mengembalikan kesalahan sisi server saat driver menguras token yang tersisa, jadi jangan abaikan saat kumpulan hasil tidak sepenuhnya dikonsumsi.
Jika Anda berhenti membaca lebih awal, tutup baris secara eksplisit dan tangani kesalahan penutupan:
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)
}
Contoh dalam artikel ini menggunakan basis data sampel AdventureWorks2025. Contoh yang berorientasi pada pembacaan membuat kueri terhadap objek bawaan seperti Sales.vSalesPerson, Production.Product, dan Sales.SalesOrderHeader. Contoh yang berorientasi pada penulisan menargetkan HumanResources.Department dan Production.ProductInventory.
Mengkueri satu baris
Gunakan QueryRowContext saat Anda mengharapkan tepat satu baris:
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)
}
Jalankan pernyataan
Gunakan ExecContext untuk INSERT, UPDATE, DELETE, dan pernyataan 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 Driver tidak mendukung LastInsertId(). Saat dipanggil, akan menghasilkan error. Gunakan OUTPUT klausa atau kueri terpisah SELECT SCOPE_IDENTITY() untuk mengambil nilai identitas yang dimasukkan.
Jika Anda menggunakan SELECT SCOPE_IDENTITY(), jalankan dalam batch atau transaksi yang sama dengan INSERT agar cakupan identitas tetap berada pada koneksi yang sama.
Jika prosedur tersimpan atau pemicu menggunakan SET NOCOUNT ON, RowsAffected() mengembalikan 0 karena SQL Server menyembunyikan pesan jumlah baris. Jika Anda memerlukan jumlah aktual, hapus SET NOCOUNT ON dari prosedur, atau kembalikan hitungan secara eksplisit melalui parameter atau SELECT pernyataan output.
Kueri dengan parameter
Selalu gunakan kueri berparameter untuk menghindari injeksi SQL. Driver mendukung baik parameter posisional maupun parameter bernama.
Important
go-mssqldb Driver menggunakan @p1, @p2, dan seterusnya untuk parameter posisi dan sql.Named() untuk parameter bernama. Sintaks placeholder ? yang digunakan oleh beberapa driver lain (seperti go-sql-driver milik MySQL) tidak berfungsi dengan nama driver sqlserver. Jika Anda bermigrasi dari database lain, ganti semua placeholder bergaya ? atau $1 dengan placeholder bergaya @p1 atau parameter bernama.
Parameter posisi
Gunakan placeholder @p1 dan @p2, lalu teruskan nilai secara berurutan:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
Parameter bernama
Gunakan sql.Named() untuk mengikat nilai ke placeholder bernama:
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"))
Beberapa kumpulan hasil
Gunakan rows.NextResultSet() untuk mengulangi beberapa kumpulan hasil yang dikembalikan oleh satu batch atau prosedur tersimpan.
Important
Anda harus menguras rows.Next() sepenuhnya untuk setiap kumpulan hasil sebelum memanggil rows.NextResultSet(). Memanggil NextResultSet() sebelum Next() mengembalikan false dan melewati baris yang tersisa tanpa pemberitahuan.
Gunakan pola loop ini untuk memproses semua kumpulan hasil dengan andal:
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
Gunakan BeginTx untuk memulai transaksi dengan tingkat isolasi tertentu. Untuk panduan transaksi yang komprehensif, termasuk tingkat isolasi, titik penyimpanan, penanganan kebuntuan, dan pola coba lagi, lihat Transaksi.
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)
}
Mendapatkan nilai identitas yang disisipkan
go-mssqldb Driver tidak mendukung LastInsertId(). Gunakan OUTPUT klausa untuk mengambil nilai identitas dalam pernyataan yang sama:
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)
Untuk beberapa baris:
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
Gunakan OFFSET dan FETCH NEXT untuk penomoran halaman sisi server. Sebuah ORDER BY klausul diperlukan:
Penomoran halaman berbasis offset
Kirimkan offset dan ukuran halaman sebagai parameter:
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()
}
Penomoran halaman keyset untuk tabel besar
Paginasi offset melambat pada tabel besar karena server harus melewati baris-baris. Paginasi keyset menggunakan kunci terakhir yang terlihat untuk mengambil halaman berikutnya secara efisien:
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
Penomoran halaman keyset secara signifikan lebih cepat daripada OFFSET/FETCH untuk halaman dalam (halaman 1000+) karena menggunakan pencarian indeks alih-alih memindai dan melewatkan baris.
Mengelompokkan beberapa pernyataan
Kirim beberapa pernyataan SQL dalam satu panggilan untuk mengurangi perjalanan bolak-balik jaringan.
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)
Memproses kumpulan hasil besar secara efisien
Untuk kueri yang mengembalikan jutaan baris, proses hasilnya secara streaming. Jangan mengumpulkan semua baris dalam memori.
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
*sql.Rows yang terbuka akan menahan koneksi dari pool hingga rows.Close() dipanggil. Untuk pemrosesan set hasil yang berlangsung sangat lama, pertimbangkan untuk membagi pekerjaan ke dalam rentang-rentang dengan menggunakan paginasi keyset agar koneksi tidak tetap terbuka selama beberapa menit.
Upsert dengan MERGE
SQL Server menggunakan pernyataan MERGE untuk operasi sisip-atau-perbarui (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))
Pernyataan yang disiapkan
Gunakan PrepareContext untuk membuat pernyataan yang disiapkan yang dapat digunakan kembali. Pernyataan yang disiapkan dapat meningkatkan performa ketika kueri yang sama berjalan berkali-kali dengan parameter yang berbeda.
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)
}
Pembatalan konteks
Semua metode database/sql menerima context.Context. Gunakan untuk batas waktu dan pembatalan.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
Jika tenggat waktu konteks berakhir, driver membatalkan kueri di server dan mengembalikan kesalahan ke penelepon.