sqlx

sqlx is a powerful extension library for Go's standard database/sql package. It positions itself between raw SQL and full ORMs, providing additional convenience features while maintaining the performance and flexibility of database/sql. sqlx adds missing features like named parameters, struct scanning, and result mapping without sacrificing control over SQL queries.

SQLGoDatabaseExtensionPostgreSQLMySQLSQLite

GitHub Overview

jmoiron/sqlx

general purpose extensions to golang's database/sql

Stars17,058
Watchers195
Forks1,108
Created:January 28, 2013
Language:Go
License:MIT License

Topics

None

Star History

jmoiron/sqlx Star History
Data as of: 7/19/2025, 09:31 AM

Library

sqlx

Overview

sqlx is a powerful extension library for Go's standard database/sql package. It positions itself between raw SQL and full ORMs, providing additional convenience features while maintaining the performance and flexibility of database/sql. sqlx adds missing features like named parameters, struct scanning, and result mapping without sacrificing control over SQL queries.

Details

sqlx enhances the standard database/sql package with features that reduce boilerplate code while preserving the ability to write and optimize SQL queries directly. It's particularly favored by developers who want more convenience than database/sql offers but don't want the overhead and abstraction of a full ORM.

Key Features

  • Struct Scanning: Automatically scan query results into structs
  • Named Parameters: Use named parameters in queries for better readability
  • Get and Select: Convenient methods for single row and multiple row queries
  • In Queries: Built-in support for IN clauses with slice expansion
  • Rebind: Automatic query rebinding for different SQL dialects
  • Transaction Support: Enhanced transaction handling with the same convenient methods

Pros and Cons

Pros

  • Minimal learning curve for developers familiar with database/sql
  • Full control over SQL queries with performance optimization capabilities
  • Lower overhead compared to full ORMs
  • Excellent for complex queries and reporting
  • Compatible with existing database/sql code
  • Support for multiple database drivers

Cons

  • No automatic schema generation or migrations
  • Manual SQL writing required for all queries
  • No built-in relationship handling
  • Less suitable for rapid prototyping compared to full ORMs
  • Requires SQL knowledge for effective use

Reference Pages

Code Examples

Basic Setup

# Initialize project
go mod init sqlx-example
go get github.com/jmoiron/sqlx
go get github.com/lib/pq          # PostgreSQL driver
go get github.com/go-sql-driver/mysql # MySQL driver
go get github.com/mattn/go-sqlite3    # SQLite driver
package main

import (
    "log"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq" // PostgreSQL driver
)

// Define structs that match your database schema
type User struct {
    ID        int    `db:"id"`
    Name      string `db:"name"`
    Email     string `db:"email"`
    Age       int    `db:"age"`
    CreatedAt string `db:"created_at"`
}

type Post struct {
    ID        int    `db:"id"`
    UserID    int    `db:"user_id"`
    Title     string `db:"title"`
    Content   string `db:"content"`
    CreatedAt string `db:"created_at"`
}

func main() {
    // Connect to database
    db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
    if err != nil {
        log.Fatalln(err)
    }
    defer db.Close()

    // Create tables (schema must be managed manually)
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        age INTEGER,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
    CREATE TABLE IF NOT EXISTS posts (
        id SERIAL PRIMARY KEY,
        user_id INTEGER REFERENCES users(id),
        title VARCHAR(200) NOT NULL,
        content TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );`
    
    db.MustExec(schema)
}

Basic Operations (CRUD)

// Create - Insert a user
user := User{
    Name:  "John Doe",
    Email: "[email protected]",
    Age:   30,
}

// Using named parameters
result, err := db.NamedExec(`
    INSERT INTO users (name, email, age) 
    VALUES (:name, :email, :age)`, user)
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId()
fmt.Printf("Created user with ID: %d\n", id)

// Alternative: Using positional parameters
_, err = db.Exec(`
    INSERT INTO users (name, email, age) 
    VALUES ($1, $2, $3)`, "Jane Smith", "[email protected]", 25)

// Read - Get a single user
var user User
err = db.Get(&user, "SELECT * FROM users WHERE id = $1", 1)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)

// Get multiple users
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE age > $1", 20)
if err != nil {
    log.Fatal(err)
}
for _, u := range users {
    fmt.Printf("User: %+v\n", u)
}

// Update - Update user
_, err = db.Exec(`
    UPDATE users SET age = $1 WHERE id = $2`, 31, 1)
if err != nil {
    log.Fatal(err)
}

// Using named parameters for update
user.Age = 32
_, err = db.NamedExec(`
    UPDATE users SET age = :age WHERE id = :id`, user)

// Delete - Delete user
_, err = db.Exec("DELETE FROM users WHERE id = $1", 1)
if err != nil {
    log.Fatal(err)
}

Advanced Querying

// Using IN clause with query expansion
ids := []int{1, 2, 3, 4, 5}
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
if err != nil {
    log.Fatal(err)
}
// Rebind for the specific database (PostgreSQL uses $1, $2, etc.)
query = db.Rebind(query)
var users []User
err = db.Select(&users, query, args...)

// Named queries with maps
m := map[string]interface{}{
    "name": "John%",
    "age":  25,
}
var users []User
nstmt, err := db.PrepareNamed(`
    SELECT * FROM users 
    WHERE name LIKE :name AND age > :age`)
err = nstmt.Select(&users, m)

// Queryx for flexible result handling
rows, err := db.Queryx("SELECT * FROM users")
for rows.Next() {
    var u User
    err = rows.StructScan(&u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", u)
}

// Using sqlx.DB methods with existing database/sql code
var name string
row := db.QueryRow("SELECT name FROM users WHERE id = $1", 1)
err = row.Scan(&name)

Transactions

// Begin a transaction
tx, err := db.Beginx()
if err != nil {
    log.Fatal(err)
}

// Use all sqlx methods within transaction
var user User
err = tx.Get(&user, "SELECT * FROM users WHERE id = $1", 1)
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// Update within transaction
_, err = tx.Exec("UPDATE users SET age = age + 1 WHERE id = $1", user.ID)
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// Create related record
_, err = tx.Exec(`
    INSERT INTO posts (user_id, title, content) 
    VALUES ($1, $2, $3)`, 
    user.ID, "New Post", "This is a transaction test")
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

// Commit transaction
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}

// Using transaction helper function
err = sqlx.Transactional(db, func(tx *sqlx.Tx) error {
    // All operations here are in a transaction
    _, err := tx.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", 
        "Bob Wilson", "[email protected]")
    if err != nil {
        return err // Automatically triggers rollback
    }
    
    var count int
    err = tx.Get(&count, "SELECT COUNT(*) FROM users")
    if err != nil {
        return err
    }
    
    if count > 100 {
        return fmt.Errorf("too many users")
    }
    
    return nil // Automatically commits
})

Prepared Statements and Named Queries

// Prepared statement
stmt, err := db.Preparex("SELECT * FROM users WHERE age > $1")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

var users []User
err = stmt.Select(&users, 25)

// Named prepared statement
nstmt, err := db.PrepareNamed("SELECT * FROM users WHERE name = :name")
if err != nil {
    log.Fatal(err)
}
defer nstmt.Close()

var user User
err = nstmt.Get(&user, map[string]interface{}{"name": "John Doe"})

// Batch operations with prepared statements
stmt, err = db.Preparex("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)")
defer stmt.Close()

users := []User{
    {Name: "User1", Email: "[email protected]", Age: 20},
    {Name: "User2", Email: "[email protected]", Age: 25},
    {Name: "User3", Email: "[email protected]", Age: 30},
}

for _, u := range users {
    _, err = stmt.Exec(u.Name, u.Email, u.Age)
    if err != nil {
        log.Printf("Failed to insert user %s: %v", u.Name, err)
    }
}

Working with NULLs and Custom Types

import (
    "database/sql"
    "github.com/jmoiron/sqlx/types"
)

type UserWithNulls struct {
    ID          int            `db:"id"`
    Name        string         `db:"name"`
    Email       string         `db:"email"`
    Age         sql.NullInt64  `db:"age"`        // Nullable integer
    Bio         sql.NullString `db:"bio"`        // Nullable string
    Preferences types.JSONText `db:"preferences"` // JSON column
}

// Insert with NULL values
user := UserWithNulls{
    Name:  "John Doe",
    Email: "[email protected]",
    Age:   sql.NullInt64{Valid: false}, // NULL
    Bio:   sql.NullString{String: "Developer", Valid: true},
    Preferences: types.JSONText(`{"theme": "dark", "notifications": true}`),
}

_, err = db.NamedExec(`
    INSERT INTO users (name, email, age, bio, preferences) 
    VALUES (:name, :email, :age, :bio, :preferences)`, user)

// Query with NULL handling
var users []UserWithNulls
err = db.Select(&users, "SELECT * FROM users WHERE age IS NULL OR age > 25")
for _, u := range users {
    if u.Age.Valid {
        fmt.Printf("User %s is %d years old\n", u.Name, u.Age.Int64)
    } else {
        fmt.Printf("User %s age is unknown\n", u.Name)
    }
}

// Custom scanner implementation
type Email string

func (e *Email) Scan(src interface{}) error {
    switch s := src.(type) {
    case string:
        *e = Email(s)
    case []byte:
        *e = Email(s)
    case nil:
        *e = ""
    default:
        return fmt.Errorf("unsupported type: %T", src)
    }
    return nil
}

Performance Optimization

// Bulk insert with single query
users := []User{
    {Name: "User1", Email: "[email protected]", Age: 20},
    {Name: "User2", Email: "[email protected]", Age: 25},
    {Name: "User3", Email: "[email protected]", Age: 30},
}

// Build bulk insert query
valueStrings := make([]string, 0, len(users))
valueArgs := make([]interface{}, 0, len(users) * 3)
for i, u := range users {
    valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d, $%d)", 
        i*3+1, i*3+2, i*3+3))
    valueArgs = append(valueArgs, u.Name, u.Email, u.Age)
}

query := fmt.Sprintf("INSERT INTO users (name, email, age) VALUES %s", 
    strings.Join(valueStrings, ","))
_, err = db.Exec(query, valueArgs...)

// Connection pooling configuration
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

// Query with context for timeout control
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var users []User
err = db.SelectContext(ctx, &users, "SELECT * FROM users")
if err != nil {
    if err == context.DeadlineExceeded {
        log.Println("Query timeout")
    }
}