使用 GORM 搭配 go-mssqldb

GORM 是一個受歡迎的 Go ORM,透過 gorm.io/driver/sqlserver 方言支援 SQL Server,並在內部使用 go-mssqldb。 GORM 處理結構差異、查詢產生和型別映射,讓你能直接用 Go 結構體,而不是寫原始 SQL。

本文介紹了 SQL Server 特有的 GORM 模式。 關於一般 GORM 的使用方式,請參閱 GORM 文件。

本文範例與 AdventureWorks2025 範例資料庫對比。

安裝 GORM 與 SQL Server 方言

建立或開啟一個 Go 模組,然後加入 GORM 和 SQL Server 方言:

mkdir go-gorm-sqlserver
cd go-gorm-sqlserver
go mod init example.com/go-gorm-sqlserver
go get gorm.io/gorm
go get gorm.io/driver/sqlserver

連接到 SQL Server

透過將 sqlserver:// DSN 傳遞給方言來開啟 GORM 工作階段:

package main

import (
    "fmt"
    "log"

    "gorm.io/driver/sqlserver"
    "gorm.io/gorm"
)

func main() {
    // Replace <server>, <user>, and <password> with your own values.
    // In production, load credentials from environment variables or a secret store.
    dsn := "sqlserver://<user>:<password>@<server>.database.windows.net:1433?database=AdventureWorks2025&encrypt=true&TrustServerCertificate=false"
    db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    sqlDB, err := db.DB()
    if err != nil {
        log.Fatal(err)
    }
    defer sqlDB.Close()

    fmt.Println("Connected to SQL Server with GORM")
}

此方言在底層使用 go-mssqldb 驅動程式,因此 連線字串 和 連線選項 中的所有連線選項都適用於 DSN。

將模型映射到現有資料表

GORM 的預設命名慣例使用小寫複數表名稱(例如 ),products這與 SQL Server 的慣例不符。 使用此 TableName() 方法將模型綁定至現有的結構限定資料表:

type Product struct {
    ProductID   int32  `gorm:"column:ProductID;primaryKey"`
    Name        string `gorm:"column:Name"`
    ProductLine *string `gorm:"column:ProductLine"`
    ListPrice   float64 `gorm:"column:ListPrice"`
}

func (Product) TableName() string {
    return "Production.Product"
}

小提示

使用現有的 SQL Server 資料庫時,請務必定義 TableName()。 依賴 GORM 的自動複數化會導致在遵循 SQL Server 命名規則的表格上出現「物件名稱無效」錯誤。

查詢數據

定義模型後,使用 GORM 的查詢方法讀取資料:

尋找帶有 WHERE 子句的列

透過 、 Where、 和 Order來篩選、排序並限制結果:Limit

var products []Product
result := db.Where("ListPrice > ?", 1000).
    Order("ListPrice DESC").
    Limit(5).
    Find(&products)
if result.Error != nil {
    log.Fatal(result.Error)
}

for _, p := range products {
    fmt.Printf("%-35s $%.2f\n", p.Name, p.ListPrice)
}

擷取單一資料列

使用 First 條件載入一條紀錄:

var product Product
if err := db.First(&product, "ProductID = ?", 680).Error; err != nil {
    log.Fatal(err)
}
fmt.Printf("%s: $%.2f\n", product.Name, product.ListPrice)

計算行數

在模型示波器上使用 Count 來取得行數:

var count int64
db.Model(&Product{}).Where("ListPrice > ?", 0).Count(&count)
fmt.Printf("Products with a list price: %d\n", count)

插入、更新與刪除資料列

寫入操作使用相同的模型結構。 本節中的範例是以 Production.ScrapReason 為對象,因此不會修改關鍵資料表。

type ScrapReason struct {
    ScrapReasonID int16     `gorm:"column:ScrapReasonID;primaryKey;autoIncrement"`
    Name          string    `gorm:"column:Name"`
    ModifiedDate  time.Time `gorm:"column:ModifiedDate"`
}

func (ScrapReason) TableName() string {
    return "Production.ScrapReason"
}

插入一列

傳送一個已填入的結構體 to Create 以插入一列並填充自動產生的主鍵:

reason := ScrapReason{Name: "GORM test scrap", ModifiedDate: time.Now()}
if err := db.Create(&reason).Error; err != nil {
    log.Fatal(err)
}
fmt.Printf("Inserted ScrapReasonID: %d\n", reason.ScrapReasonID)

// Clean up
db.Delete(&reason)

更新資料列

使用 Model with Update 來更改欄位值:

reason := ScrapReason{Name: "GORM update test", ModifiedDate: time.Now()}
db.Create(&reason)

db.Model(&reason).Update("Name", "GORM updated scrap")

var updated ScrapReason
db.First(&updated, reason.ScrapReasonID)
fmt.Printf("Updated name: %s\n", updated.Name)

// Clean up
db.Delete(&reason)

刪除資料列

將已填入主鍵的結構體傳遞給 Delete:

reason := ScrapReason{Name: "GORM delete test", ModifiedDate: time.Now()}
db.Create(&reason)
id := reason.ScrapReasonID

db.Delete(&reason)

var count int64
db.Model(&ScrapReason{}).Where("ScrapReasonID = ?", id).Count(&count)
fmt.Printf("Rows remaining with that ID: %d\n", count)

批次插入與 2,100 參數限制

SQL Server 每個語句限制sp_executesql為 2,100 個參數。 GORM 的批次插入會產生一個多資料列的 VALUES 子句,其中每個欄位都會占用一個參數,因此,對於有 10 個欄位的資料表來說,每批大約在 210 列時就會達到上限。

在工作階段中設定 CreateBatchSize,以保持在限制範圍內:

reasons := make([]ScrapReason, 50)
for i := range reasons {
    reasons[i] = ScrapReason{
        Name:         fmt.Sprintf("Batch scrap %d", i+1),
        ModifiedDate: time.Now(),
    }
}

// 2 inserted columns per row (Name, ModifiedDate; the autoIncrement ID is excluded),
// so batches of 500 are well within the 2,100-parameter limit.
result := db.CreateInBatches(&reasons, 500)
if result.Error != nil {
    log.Fatal(result.Error)
}
fmt.Printf("Inserted %d rows in batches\n", result.RowsAffected)

// Clean up
for _, r := range reasons {
    db.Delete(&r)
}

對於多欄位的資料表,請按比例減少批次大小。 一個安全的公式: batch size = floor(2100 / number of inserted columns)。

Transactions

在 GORM 交易中包裝多個操作:

err := db.Transaction(func(tx *gorm.DB) error {
    r1 := ScrapReason{Name: "TX scrap 1", ModifiedDate: time.Now()}
    if err := tx.Create(&r1).Error; err != nil {
        return err
    }

    r2 := ScrapReason{Name: "TX scrap 2", ModifiedDate: time.Now()}
    if err := tx.Create(&r2).Error; err != nil {
        return err
    }

    // Both inserts commit together.
    fmt.Printf("Inserted IDs: %d, %d\n", r1.ScrapReasonID, r2.ScrapReasonID)

    // Clean up inside the same transaction.
    tx.Delete(&r1)
    tx.Delete(&r2)

    return nil
})
if err != nil {
    log.Fatal(err)
}
fmt.Println("Transaction committed")

如果回呼回傳錯誤,GORM 會自動復原。

原始 SQL 備援

當 GORM 的查詢建構器無法產生你需要的 SQL Server 語法時,請在使用 GORM 會話的同時切換到 raw SQL:

var results []struct {
    Name      string
    ListPrice float64
}
result := db.Raw("SELECT TOP 5 Name, ListPrice FROM Production.Product WHERE ListPrice > @price ORDER BY ListPrice DESC",
    sql.Named("price", 1000)).Scan(&results)
if result.Error != nil {
    log.Fatal(result.Error)
}

for _, r := range results {
    fmt.Printf("%-35s $%.2f\n", r.Name, r.ListPrice)
}

原始查詢使用由 GORM 管理的同一個連線池,因此池 db.DB() 的設定仍然適用。

設定連線池

存取底層 *sql.DB 資料以設定池參數:

sqlDB, err := db.DB()
if err != nil {
    log.Fatal(err)
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

關於池子大小的指引,請參見 「連結池化」。

AutoMigrate 注意事項

AutoMigrate 建立或修改表格以符合你的模型定義。 它能搭配 SQL Server 運作,但請注意以下行為:

  • 除非你使用結構描述前綴覆寫 AutoMigrate,否則 dbo 會在預設結構描述(TableName())中建立資料表。
  • 它會新增缺失欄位並建立索引,但不會刪除欄位或更改現有欄位類型。
  • SQL Server 的索引名稱有 128 個字元的限制。 GORM 產生的索引名稱在長名稱或複合索引的表格中可能超過此限制。 如果達到這個限制,請使用明確 gorm:"index:idx_short_name" 標籤。

對於有管理結構的生產資料庫,建議使用 遷移工具 而非 AutoMigrate。