Usa sqlx con go-mssqldb

sqlx è una libreria leggera che estende le database/sql di Go con la scansione di struct, l'associazione di parametri con nome e helper per query. Funziona con qualsiasi database/sql driver, incluso go-mssqldb.

A differenza di un ORM, SQL non genera SQL né gestisce schemi. Scrivi le query e sqlx gestisce la mappatura tra i set di risultati e le struct Go. Questo equilibrio lo rende adatto quando vuoi meno boilerplate rispetto al raw database/sql , senza l'astrazione di un ORM completo.

Gli esempi in questo articolo vengono confrontati con il database di esempio AdventureWorks2025 .

Installa sqlx

Crea o apri un modulo Go, poi aggiungi sqlx e go-mssqldb:

mkdir go-sqlx-sqlserver
cd go-sqlx-sqlserver
go mod init example.com/go-sqlx-sqlserver
go get github.com/jmoiron/sqlx
go get github.com/microsoft/go-mssqldb

Connessione a SQL Server

Apri una connessione usando sqlx.Open con il nome del driver sqlserver:

package main

import (
    "fmt"
    "log"

    "github.com/jmoiron/sqlx"
    _ "github.com/microsoft/go-mssqldb"
)

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 := sqlx.Open("sqlserver", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to SQL Server with sqlx")
}

sqlx.Open restituisce un *sqlx.DB che incapsula il *sql.DB standard. Tutti i database/sql metodi funzionano ancora, oltre alle estensioni SQL descritte in questo articolo.

Scansione delle struct con Select e Get

Il principale vantaggio di SQL rispetto a RAW database/sql è la scansione automatica delle strutture. Associare le colonne ai campi della struct con i tag db:

type Product struct {
    ProductID int32   `db:"ProductID"`
    Name      string  `db:"Name"`
    ListPrice float64 `db:"ListPrice"`
}

Seleziona più righe

Select scansiona tutte le righe di risultati in una fetta di struct:

var products []Product
err := db.Select(&products,
    "SELECT TOP 5 ProductID, Name, ListPrice FROM Production.Product WHERE ListPrice > @p1 ORDER BY ListPrice DESC",
    sql.Named("p1", 1000))
if err != nil {
    log.Fatal(err)
}
for _, p := range products {
    fmt.Printf("%-35s $%.2f\n", p.Name, p.ListPrice)
}

Select esegue la query, legge ogni riga in una struttura e chiude l'iteratore di righe. Sostituisce lo schema Query / for rows.Next() / rows.Scan().

Ottieni una singola riga

Get scansiona la prima riga in una singola struzione:

var product Product
err := db.Get(&product,
    "SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = @p1",
    sql.Named("p1", 680))
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s: $%.2f\n", product.Name, product.ListPrice)

Get ritorna sql.ErrNoRows se la query non restituisce righe. Se più righe corrispondono, viene scansionata solo la prima.

Parametri denominati

sqlx supporta parametri denominati in stile :name usando mappe o struct come origine dei parametri. Tuttavia, go-mssqldb utilizza @name i parametri nativamente, quindi per SQL Server, usa sql.Named con i segnaposto standard @p1 come mostrato negli esempi precedenti.

Se preferisci la sintassi di :name SQLX, usa sqlx.Named per ribindare:

query, args, err := sqlx.Named(
    "SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = :id",
    map[string]interface{}{"id": 680})
if err != nil {
    log.Fatal(err)
}
// Rebind to positional placeholders for go-mssqldb
query = db.Rebind(query)

var product Product
err = db.Get(&product, query, args...)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s: $%.2f\n", product.Name, product.ListPrice)

Annotazioni

sqlx.Named converte i segnaposto :name in parametri posizionali ?. Rebind poi converte ? nel formato del driver di destinazione. Usa questo approccio in due fasi quando vuoi parametri portatili con nomi tra i driver.

Espansione della clausola IN

Costruire WHERE column IN (...) clausole con un numero variabile di valori è noioso con SQL grezzo. Usa sqlx.In per espandere una slice nei singoli segnaposto:

ids := []int32{680, 706, 707}
query, args, err := sqlx.In(
    "SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID IN (?)", ids)
if err != nil {
    log.Fatal(err)
}
query = db.Rebind(query)

var products []Product
err = db.Select(&products, query, args...)
if err != nil {
    log.Fatal(err)
}
for _, p := range products {
    fmt.Printf("%d: %s ($%.2f)\n", p.ProductID, p.Name, p.ListPrice)
}

sqlx.In espande il singolo ? e ?, ?, ? appiattisce la fetta nella lista ARGs. Rebind si converte quindi nel formato segnaposto del driver.

Eseguire scritture

Usa Exec per le istruzioni INSERT, UPDATE e DELETE. Questi esempi si riferiscono a Production.ScrapReason in modo da non modificare le tabelle critiche.

Attenzione

sqlx offre anche MustExec, che genera un panic invece di restituire un errore. Evita MustExec i servizi di produzione perché un panico non recuperato interrompe il processo. Gli esempi in questa sezione utilizzano Exec con controlli espliciti degli errori.

Inserisci una riga

Usa Exec con un'istruzione INSERT e parametri nominati:

result, err := db.Exec(
    "INSERT INTO Production.ScrapReason (Name, ModifiedDate) VALUES (@p1, @p2)",
    sql.Named("p1", "sqlx test scrap"), sql.Named("p2", time.Now()))
if err != nil {
    log.Fatal(err)
}
affected, _ := result.RowsAffected()
fmt.Printf("Inserted %d row(s)\n", affected)

// Clean up
db.Exec("DELETE FROM Production.ScrapReason WHERE Name = @p1",
    sql.Named("p1", "sqlx test scrap"))

Transazione con SQL

Incapsulare le operazioni in una transazione usando Beginx:

tx, err := db.Beginx()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback() // no-op after Commit, but essential if an error occurs

if _, err := tx.Exec(
    "INSERT INTO Production.ScrapReason (Name, ModifiedDate) VALUES (@p1, @p2)",
    sql.Named("p1", "sqlx tx scrap 1"), sql.Named("p2", time.Now())); err != nil {
    log.Fatal(err)
}
if _, err := tx.Exec(
    "INSERT INTO Production.ScrapReason (Name, ModifiedDate) VALUES (@p1, @p2)",
    sql.Named("p1", "sqlx tx scrap 2"), sql.Named("p2", time.Now())); err != nil {
    log.Fatal(err)
}

if err := tx.Commit(); err != nil {
    log.Fatal(err)
}
fmt.Println("Transaction committed")

// Clean up
db.Exec("DELETE FROM Production.ScrapReason WHERE Name IN (@p1, @p2)",
    sql.Named("p1", "sqlx tx scrap 1"), sql.Named("p2", "sqlx tx scrap 2"))

La defer tx.Rollback() chiamata garantisce che la transazione venga ripulita se qualche operazione fallisce. Dopo un Commit riuscito, Rollback è un'operazione nulla sicura. L'oggetto tx fornisce gli stessi helper sqlx (Select, Get, Exec) limitati all'ambito della transazione.

Scansione struct con join

SQL può scansionare colonne da query congiunte in una struct piatta o in una struct incorporata.

type ProductWithCategory struct {
    ProductID    int32   `db:"ProductID"`
    ProductName  string  `db:"ProductName"`
    CategoryName string  `db:"CategoryName"`
    ListPrice    float64 `db:"ListPrice"`
}

var results []ProductWithCategory
err := db.Select(&results, `
    SELECT TOP 5
        p.ProductID,
        p.Name AS ProductName,
        pc.Name AS CategoryName,
        p.ListPrice
    FROM Production.Product p
    JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
    JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
    WHERE p.ListPrice > @p1
    ORDER BY p.ListPrice DESC`,
    sql.Named("p1", 1000))
if err != nil {
    log.Fatal(err)
}
for _, r := range results {
    fmt.Printf("%-35s %-15s $%.2f\n", r.ProductName, r.CategoryName, r.ListPrice)
}

Quando scegliere sqlx vs GORM vs database/sql

Scegliere... Quando si adatta meglio
database/sql Vuoi zero dipendenze oltre al driver, oppure hai bisogno di avere il pieno controllo su ogni query.
sqlx Vuoi la scansione di struct e strumenti di supporto per le query senza il livello di astrazione di un ORM.
GORM Vuoi gestione degli schemi, associazioni, hook e un set completo di funzionalità ORM.