GORM と go-mssqldb を組み合わせて使う

GORM はgorm.io/driver/sqlserver方言でSQL Serverをサポートする人気の Go ORM で、内部で go-mssqldb を使用しています。 GORM はスキーマの違い、クエリ生成、型マッピングを処理しているので、生のSQLを書く代わりにGo構造体で作業できます。

この記事では、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"
}

Tip

既存の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)
}

1 つの行を取得する

Firstを条件付きで1つのレコードを読み込む:

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

行を挿入してください

値が設定された構造体を 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 を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 1文あたりsp_executesqlパラメータ数を2,100に制限しています。 GORM のバッチ挿入は、各列が1つのパラメータを消費する多行 VALUES 節を生成するため、10列のテーブルは1バッチあたり約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 セッションを使いながら生の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(ORM管理)と同じ接続プールを使用しているため、 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を好む。