Use GORM com go-mssqldb

GORM é um ORM popular de Go que oferece suporte ao SQL Server por meio do dialeto gorm.io/driver/sqlserver, que usa go-mssqldb internamente. O GORM lida com diferenças de esquema, geração de consultas e mapeamento de tipos para que você possa trabalhar com structs do Go em vez de escrever SQL bruto.

Este artigo aborda padrões GORM específicos do SQL Server. Para uso geral do GORM, veja a documentação do GORM.

Os exemplos neste artigo usam o banco de dados de exemplo AdventureWorks2025.

Instalar o GORM e o dialeto do SQL Server

Crie ou abra um módulo Go, depois adicione o GORM e o dialeto do 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

Conectar-se ao SQL Server

Abra uma sessão do GORM passando um DSN sqlserver:// ao dialeto:

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

O dialeto usa internamente o driver go-mssqldb, então todas as opções de conexão de Cadeias de conexão e Opções de conexão se aplicam ao DSN.

Mapear modelos para tabelas existentes

A convenção padrão de nomes do GORM usa nomes de tabelas plurais minúsculas (por exemplo, products), que não correspondem às convenções do SQL Server. Use o método TableName() para vincular um modelo a uma tabela existente qualificada por esquema:

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

Dica

Sempre defina TableName() quando você trabalha com bancos de dados SQL Server existentes. Confiar na pluralização automática do GORM resulta em erros de "nome de objeto inválido" em tabelas que seguem as convenções de nomenclatura do SQL Server.

Consultar dados

Depois de definir um modelo, use os métodos de consulta do GORM para ler dados:

Encontre linhas com uma cláusula WHERE

Filtre, ordene e limite os resultados usando Where, Order, e 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)
}

Recuperar uma única linha

Use First com uma condição para carregar um registro:

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)

Contar linhas

Use Count em um escopo de modelo para obter uma contagem de linhas:

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

Inserir, atualizar e excluir linhas

Operações de escrita usam as mesmas estruturas de modelo. Os exemplos nesta seção apontam para Production.ScrapReason para que não modifiquem tabelas críticas.

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

Insira uma linha

Passe uma estrutura preenchida para Create inserir uma linha e preencha a chave primária gerada automaticamente:

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)

Atualizar uma linha

Use Model com Update para alterar o valor de uma coluna:

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)

Excluir uma linha

Passe uma estrutura com uma chave primária preenchida para 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)

Inserção em lote e o limite de 2.100 parâmetros

O SQL Server limita sp_executesql a 2.100 parâmetros por instrução. O inserto por lote do GORM gera uma cláusula de várias linhas VALUES onde cada coluna consome um parâmetro, então uma tabela com 10 colunas atinge o limite de aproximadamente 210 linhas por lote.

Defina CreateBatchSize na sessão para permanecer dentro do limite:

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

Para tabelas com muitas colunas, reduza o tamanho do lote proporcionalmente. Uma fórmula segura: batch size = floor(2100 / number of inserted columns).

Transactions

Agrupe várias operações em uma transação do 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")

Se o retorno de chamada retornar um erro, o GORM reverte automaticamente.

Fallback de SQL bruto

Quando o construtor de consultas do GORM não gera a sintaxe do SQL Server que você precisa, passe para SQL bruto enquanto ainda usa a sessão do GORM:

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

Consultas brutas usam o mesmo pool de conexões gerenciado pelo GORM, então as configurações do db.DB() pool ainda se aplicam.

Configure o pool de conexões

Acesse o subjacente *sql.DB para definir parâmetros do pool:

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

Para orientações sobre o dimensionamento do pool de conexões, consulte Pool de conexões.

Considerações sobre AutoMigrate

AutoMigrate cria ou altera tabelas para corresponder às definições do seu modelo. Ele funciona com SQL Server, mas fique atento a estes comportamentos:

  • AutoMigrate cria tabelas no esquema padrão (dbo), a menos que você substitua TableName() com um prefixo de esquema.
  • Ele adiciona colunas faltantes e cria índices, mas não elimina colunas nem altera tipos de colunas existentes.
  • O SQL Server tem um limite de 128 caracteres para nomes de índice. Os nomes de índices gerados pelo GORM podem exceder esse limite em tabelas com nomes longos ou índices compostos. Use tags explícitas gorm:"index:idx_short_name" caso atinja esse limite.

Para bancos de dados de produção com esquemas gerenciados, prefira ferramentas de migração a AutoMigrate.