go-mssqldb ile sorgular ve ifadeler

Sürücü, go-mssqldb sorguları çalıştırmak ve ifadeleri çalıştırmak için standart database/sql arayüzü kullanır. Bu makale, sürücüyle veri erişimi için yaygın kalıpları ele alır.

SELECT sorgusu çalıştırma

Satır döndüren bir sorguyu çalıştırmak için kullanılır 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

Her zaman rows.Close() öğesini çağırın (genellikle defer ile) ve döngüden sonra rows.Err() öğesini kontrol edin. Sıraları kapatmamak, havuzdan bağlantıların sızmasına neden olabilir. rows.Close() Ayrıca sürücü kalan tokenları tüketirken sunucu tarafında bir hata döndürebilir, bu yüzden sonuç seti tamamen tüketilmediğinde bunu görmezden gelmeyin.

Erken okumayı bırakırsanız, satırları açıkça kapatın ve kapanış hatasını ele alın:

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

Bu makaledeki örnekler AdventureWorks2025 örnek veritabanıyla karşılaştırılıyor. Okuma odaklı örnekler, , Sales.vSalesPerson, ve Production.Productgibi Sales.SalesOrderHeaderyerleşik nesneleri sorgular. Yazma odaklı örnekler, HumanResources.Department ve Production.ProductInventory için tasarlanmıştır.

Tek bir satırı sorgulama

Tam olarak bir satır beklediğinizde QueryRowContext kullanın:

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

Bir komutu çalıştır

ExecContext, INSERT, UPDATE ve DDL deyimleri için DELETE kullanın:

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 sürücüsü, LastInsertId() öğesini desteklemiyor. Onu çağırmak hata veriyor. Eklenen bir kimlik değerini elde etmek için bir OUTPUT yan tümce veya ayrı bir SELECT SCOPE_IDENTITY() sorgu kullanın.

SELECT SCOPE_IDENTITY() kullanıyorsanız, kimlik kapsamının aynı bağlantıda kalması için onu INSERT ile aynı toplu işte veya işlemde çalıştırın.

Bir saklı yordam veya tetikleyici SET NOCOUNT ON kullanıyorsa, SQL Server satır sayısı iletisini gizlediği için RowsAffected() 0 döndürür. Gerçek sayıya ihtiyacınız varsa, ya prosedürden çıkarın SET NOCOUNT ON ya da sayıyı bir çıkış parametresi veya SELECT ifadesi aracılığıyla açıkça geri döndürün.

Parametreli sorgular

SQL enjeksiyonundan kaçınmak için her zaman parametrizlenmiş sorgular kullanın. Sürücü hem konumsal hem de adlandırılmış parametreleri destekler.

Important

go-mssqldb sürücüsü, konumsal parametreler için @p1, @p2 ve benzerlerini, adlandırılmış parametreler içinse sql.Named() kullanır. ? Bazı diğer sürücülerin (örneğin MySQL'ingo-sql-driver) kullandığı yer tutucu sözdizimi sürücü sqlserver adıyla çalışmaz. Başka bir veritabanından taşınıyorsanız, tüm ? veya $1-style yer tutucularını @p1-style veya adlandırılmış parametrelerle değiştirin.

Konumsal parametreler

@p1 ve @p2 yer tutucularını kullanın ve değerleri sırayla iletin:

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

Adlandırılmış parametreler

Değerleri adlandırılmış yer tutuculara bağlamak için kullanılır 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"))

Birden fazla sonuç kümesi

rows.NextResultSet(), tek bir işlem grubu veya saklı yordamın döndürdüğü birden fazla sonuç kümesinde yineleme yapmak için kullanılır.

Important

rows.Next() çağrısından önce her sonuç kümesi için rows.NextResultSet() işlemini tamamen tüketmeniz gerekir. NextResultSet() öğesinden önce Next() çağrılması, false döndürür ve kalan satırları sessizce atlar.

Tüm sonuç kümelerini güvenilir şekilde işlemek için bu döngü desenini kullanın:

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

Belirli bir izolasyon seviyesinde işlem başlatmak için kullanılır BeginTx . İzolasyon seviyeleri, kayıt noktaları, deadlock yönetimi ve yeniden deneme kalıpları dahil olmak üzere kapsamlı işlem rehberliği için Transactions'a bakınız.

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

Eklenen kimlik değerlerini alın

go-mssqldb sürücüsü, LastInsertId() öğesini desteklemiyor. Aynı ifade içinde kimlik değerini almak için OUTPUT yan tümcesini kullanın:

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)

Birden fazla satır için:

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

Sunucu taraflı sayfalama için OFFSET ve FETCH NEXT kullanın. Bir ORDER BY madde gereklidir:

Ofset tabanlı sayfalama

Ofset ve sayfa boyutunu parametre olarak verin:

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

Büyük tablolar için anahtar kümesiyle sayfalama

Büyük tablolarda ofset sayfalandırma yavaşlar çünkü sunucu satır atlamak zorundadır. Anahtar seti sayfalaması, bir sonraki sayfayı verimli bir şekilde almak için son görülen anahtarı kullanır:

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

İpucu

Anahtar seti sayfalaması, derin sayfalara (sayfa 1000+) göre OFFSET/FETCH önemli ölçüde daha hızlıdır çünkü satır tarama ve atlama yerine indeks araması kullanır.

Birden çok ifadeyi toplu işleme

Ağ gidiş-dönüş mesafelerini azaltmak için tek bir çağrıda birden fazla SQL ifadesi gönderin.

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)

Büyük sonuç kümelerini verimli şekilde işleyin

Milyonlarca satır döndüren sorgular için sonuçları akış biçiminde işleyin. Tüm satırları hafızada biriktirme.

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

Dikkat

Açık *sql.Rows bir bağlantı, havuzdan çağrılana rows.Close() kadar bir bağlantı açar. Sonuç kümesinin çok uzun süren işlenmesi durumunda, bağlantıyı dakikalar boyunca açık tutmamak için işi anahtar tabanlı sayfalama kullanarak aralıklara bölmeyi değerlendirin.

Upsert ile MERGE

SQL Server, insert veya update (upsert) işlemleri için bu MERGE ifadeyi kullanır.

_, 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))

Hazırlanmış ifadeler

Yeniden kullanılabilir hazırlanmış bir ifade oluşturmak için kullanın PrepareContext . Hazırlanmış ifadeler, aynı sorgu farklı parametrelerle birçok kez çalıştığında performansı artırabilir.

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

Bağlam iptal

Tüm database/sql yöntemleri, bir context.Context kabul eder. Mola ve iptal için kullanın.

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

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

Bağlamın zaman aşımı süresi dolarsa, sürücü sunucudaki sorguyu iptal eder ve çağırana bir hata döndürür.