Usa GORM con go-mssqldb

GORM es un ORM popular para Go que admite SQL Server a través del dialecto gorm.io/driver/sqlserver, que utiliza go-mssqldb internamente. GORM gestiona diferencias de esquemas, generación de consultas y mapeo de tipos para que puedas trabajar con estructuras Go en lugar de escribir SQL en bruto.

Este artículo trata sobre patrones GORM específicos de SQL Server. Para el uso general de GORM, consulte la documentación de GORM.

Los ejemplos de este artículo se ejecutan con la base de datos de ejemplo AdventureWorks2025.

Instala GORM y el dialecto de SQL Server

Crea o abre un módulo de Go, luego añade GORM y el dialecto de 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

Conectarse a SQL Server

Abre una sesión de GORM pasando un sqlserver:// DSN al dialecto:

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

El dialecto utiliza el controlador go-mssqldb en segundo plano, por lo que todas las opciones de conexión de Cadenas de conexión y Opciones de conexión se aplican al DSN.

Mapear modelos a tablas existentes

La convención de nombres por defecto de GORM utiliza nombres de tablas en minúsculas plurales (por ejemplo, products), que no coinciden con las convenciones de SQL Server. Utiliza el TableName() método para vincular un modelo a una tabla ya válida para el 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"
}

Tip

Define TableName() siempre cuándo trabajas con bases de datos de SQL Server existentes. Confiar en la pluralización automática de GORM genera errores de "nombre de objeto no válido" al trabajar con tablas que siguen las convenciones de nomenclatura de SQL Server.

Consultar datos

Después de definir un modelo, utiliza los métodos de consulta de GORM para leer datos:

Busca filas con una cláusula WHERE

Filtrar, ordenar y limitar los resultados usando Where, Order, y 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 una sola fila

Úsalo First con una condición para cargar un 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)

Recuento de filas

Usa Count en un ámbito del modelo para obtener el número de filas:

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

Insertar, actualizar y eliminar filas

Las operaciones de escritura usan las mismas estructuras de modelo. Los ejemplos de esta sección apuntan a Production.ScrapReason para que no modifiquen tablas 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"
}

Insertar una fila

Pasa una estructura rellenada a Create para insertar una fila y rellenar la clave primaria autogenerada:

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)

Actualizar una fila

Úsase Model con Update para cambiar el valor de una columna:

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)

Eliminar una fila

Pasa una estructura con una clave primaria poblada a 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)

Inserción por lotes y el límite de 2.100 parámetros

SQL Server limita sp_executesql a 2.100 parámetros por sentencia. El inserto por lotes de GORM genera una cláusula de varias filas VALUES donde cada columna consume un parámetro, por lo que una tabla con 10 columnas alcanza el límite de aproximadamente 210 filas por lote.

Establece CreateBatchSize en la sesión para no superar el límite:

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 tablas con muchas columnas, reduce proporcionalmente el tamaño del lote. Una fórmula segura: batch size = floor(2100 / number of inserted columns).

Transactions

Agrupa varias operaciones en una transacción de 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")

Si la llamada de retorno devuelve un error, GORM revierte la operación automáticamente.

Recurso de SQL sin procesar

Cuando el generador de consultas de GORM no genera la sintaxis de SQL Server que necesitas, pasa a SQL en bruto mientras sigues usando la sesión de 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)
}

Las consultas sin procesar usan el mismo pool de conexiones gestionado por GORM, por lo que la configuración del pool de db.DB() se sigue aplicando.

Configurar el pool de conexiones

Accede al subyacente *sql.DB para establecer los parámetros del pool:

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

Para obtener indicaciones sobre el dimensionamiento del grupo de conexiones, consulta Grupo de conexiones.

Consideraciones sobre AutoMigrate

AutoMigrate crea o modifica tablas para que coincidan con las definiciones de tu modelo. Funciona con SQL Server, pero ten en cuenta estos comportamientos:

  • AutoMigrate crea tablas en el esquema por defecto (dbo) a menos que anules TableName() con un prefijo de esquema.
  • Añade columnas faltantes y crea índices, pero no elimina columnas ni cambia los tipos de columna existentes.
  • SQL Server tiene un límite de 128 caracteres en nombres de índice. Los nombres de índices generados por GORM pueden superar este límite en tablas con nombres largos o índices compuestos. Usa etiquetas explícitas gorm:"index:idx_short_name" si alcanzas ese límite.

Para bases de datos de producción con esquemas gestionados, se prefieren herramientas de migración sobre AutoMigrate.