Skip to content

db.Repository

Repository pattern implementation with blueprint and goqu. The Repository provides a comprehensive interface-driven abstraction for database operations, composed of multiple specialized interfaces for different operation types.

Overview

The Repository interface is composed of multiple smaller interfaces, following the Interface Segregation Principle:

type Repository interface {
    Identifier
    Builder
    Reader
    Executor
    Writer
    Deleter
    Updater
    Counter
    GridOps
    SqlBuilder
    NewTransaction(opts *sql.TxOptions) (Transaction, error)
}

Usage

package main

import (
    "context"
    "fmt"
    "github.com/oddbit-project/blueprint/db"
    "github.com/oddbit-project/blueprint/provider/pgsql"
    "log"
    "time"
)

type UserRecord struct {
    Id        int       `db:"id_user" goqu:"skipinsert"` // field is autogenerated
    CreatedAt time.Time `db:"created_at"`
    UpdatedAt time.Time `db:"updated_at"`
    Name      string    `db:"name"`
    Email     string    `db:"email"`
}

func main() {
    pgConfig := pgsql.NewClientConfig() // use defaults
    pgConfig.DSN = "postgres://username:password@localhost:5432/database?sslmode=allow"

    client, err := pgsql.NewClient(pgConfig)
    if err != nil {
        log.Fatal(err)
    }

    // create a repository for the table users
    // Note: context is internally stored and then propagated to the appropriate sqlx methods; this is
    // not the advised way of using contexts, but the rationale is to allow clean thread or application shutdown
    // via context, without the overhead of adding an extra parameter to every function
    repo := db.NewRepository(context.Background(), client, "users")

    user1 := &UserRecord{
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
        Name:      "John Connor",
        Email:     "jconnor@skynet.com",
    }

    // Add user
    if err = repo.Insert(user1); err != nil {
        log.Fatal(err)
    }

    // Read all users
    users := make([]*UserRecord, 0)
    if err = repo.Fetch(repo.SqlSelect(), &users); err != nil {
        log.Fatal(err)
    }

    // search for sarah by email
    sarah := &UserRecord{}
    if err = repo.FetchRecord(map[string]any{"email": "sconnor@skynet.com"}, sarah); err != nil {
        if db.EmptyResult(err) {
            fmt.Println("Sarah Connor not found")
        } else {
            log.Fatal(err)
        }

    }

    // Count all users
    count, err := repo.Count()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Total users: %d\n", count)

    // Count users with specific criteria
    activeCount, err := repo.CountWhere(map[string]any{"active": true})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Active users: %d\n", activeCount)
}

Interface Documentation

Reader Interface

The Reader interface provides methods for fetching data from the database:

type Reader interface {
    FetchOne(qry *goqu.SelectDataset, target any) error
    FetchRecord(fieldValues map[string]any, target any) error
    Fetch(qry *goqu.SelectDataset, target any) error
    FetchWhere(fieldValues map[string]any, target any) error
    FetchByKey(keyField string, value any, target any) error
    Exists(fieldName string, fieldValue any, skip ...any) (bool, error)
}

FetchOne

func (r *repository) FetchOne(qry *goqu.SelectDataset, target any) error

Fetches a single record using a goqu SelectDataset. The target must be a struct pointer. Automatically adds LIMIT 1 to the query.

Example:

user := &UserRecord{}
err := repo.FetchOne(repo.SqlSelect().Where(goqu.C("id").Eq(123)), user)
if err != nil {
    if db.EmptyResult(err) {
        log.Println("User not found")
    } else {
        log.Fatal(err)
    }
}

FetchRecord

func (r *repository) FetchRecord(fieldValues map[string]any, target any) error

Fetches a single record with WHERE clauses built from field values. All conditions are combined with AND.

Example:

user := &UserRecord{}
err := repo.FetchRecord(map[string]any{
    "email": "john@example.com",
    "active": true,
}, user)

Fetch

func (r *repository) Fetch(qry *goqu.SelectDataset, target any) error

Fetches multiple records. The target must be a slice pointer.

Example:

var users []*UserRecord
err := repo.Fetch(
    repo.SqlSelect().
        Where(goqu.C("active").IsTrue()).
        OrderBy(goqu.C("created_at").Desc()),
    &users,
)

FetchWhere

func (r *repository) FetchWhere(fieldValues map[string]any, target any) error

Fetches multiple records with WHERE clauses from field values map.

Example:

var users []*UserRecord
err := repo.FetchWhere(map[string]any{
    "department": "engineering",
    "active": true,
}, &users)

FetchByKey

func (r *repository) FetchByKey(keyField string, value any, target any) error

Fetches a single record by a specific key field value.

Example:

user := &UserRecord{}
err := repo.FetchByKey("id", 123, user)

Exists

func (r *repository) Exists(fieldName string, fieldValue any, skip ...any) (bool, error)

Checks if records exist matching the given criteria. The optional skip parameter allows excluding specific records (useful for unique constraint validation).

Example:

// Check if email exists
exists, err := repo.Exists("email", "john@example.com")

// Check if email exists, excluding user with ID 123
exists, err = repo.Exists("email", "john@example.com", "id", 123)

Counter Interface

The Counter interface provides methods for counting records:

type Counter interface {
    Count() (int64, error)
    CountWhere(fieldValues map[string]any) (int64, error)
}

Count

func (r *repository) Count() (int64, error)

Returns the total number of rows in the table.

Example:

total, err := repo.Count()
if err != nil {
    log.Fatal(err)
}
log.Printf("Total records: %d", total)

CountWhere

func (r *repository) CountWhere(fieldValues map[string]any) (int64, error)

Returns the number of rows matching the field values map. All conditions are combined with AND.

Example:

activeCount, err := repo.CountWhere(map[string]any{
    "status": "active",
    "verified": true,
})

Executor Interface

The Executor interface provides methods for executing queries and raw SQL:

type Executor interface {
    Exec(qry *goqu.SelectDataset) error
    RawExec(sql string, args ...any) error
    Select(sql string, target any, args ...any) error
}

Exec

func (r *repository) Exec(qry *goqu.SelectDataset) error

Executes a query without returning results (useful for complex operations).

Example:

err := repo.Exec(
    repo.SqlSelect().
        From(goqu.L("generate_series(1, 10)")),
)

RawExec

func (r *repository) RawExec(sql string, args ...any) error

Executes raw SQL that doesn't return rows (DDL, complex updates, stored procedures).

Example:

err := repo.RawExec(`
    CREATE INDEX CONCURRENTLY idx_users_email_active 
    ON users(email) WHERE active = true
`)

// With parameters
err = repo.RawExec(
    "CALL process_user_batch($1, $2)", 
    startDate, endDate,
)

Select

func (r *repository) Select(sql string, target any, args ...any) error

Executes raw SQL SELECT queries and scans results into target.

Example:

var stats []struct {
    Department string `db:"department"`
    Count      int    `db:"count"`
}

err := repo.Select(`
    SELECT department, COUNT(*) as count 
    FROM users 
    WHERE active = $1 
    GROUP BY department
`, &stats, true)

Writer Interface

The Writer interface provides methods for inserting records:

type Writer interface {
    Insert(records ...any) error
    InsertReturning(record any, returnFields []string, target ...any) error
}

Insert

func (r *repository) Insert(records ...any) error

Inserts one or more records. Supports efficient batch insert operations that generate optimized SQL for multiple records.

Single Record Example:

user := &UserRecord{
    Name:  "John Doe",
    Email: "john@example.com",
}
err := repo.Insert(user)

Batch Insert Examples:

Method 1: Direct []any slice

users := []any{
    &UserRecord{Name: "Alice", Email: "alice@example.com"},
    &UserRecord{Name: "Bob", Email: "bob@example.com"},
    &UserRecord{Name: "Charlie", Email: "charlie@example.com"},
}
err := repo.Insert(users...)

Method 2: Using ToAnySlice helper

// With typed slice
typedUsers := []*UserRecord{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}

// Convert and insert
err := repo.Insert(db.ToAnySlice(typedUsers)...)

Method 3: Large datasets with chunking

func batchInsertLarge(repo db.Repository, users []*UserRecord) error {
    const chunkSize = 1000

    for i := 0; i < len(users); i += chunkSize {
        end := i + chunkSize
        if end > len(users) {
            end = len(users)
        }

        chunk := users[i:end]
        records := make([]any, len(chunk))
        for j, user := range chunk {
            records[j] = user
        }

        if err := repo.Insert(records...); err != nil {
            return fmt.Errorf("chunk %d-%d failed: %w", i, end-1, err)
        }
    }
    return nil
}

Method 4: Batch insert with transaction

func batchInsertWithTransaction(repo db.Repository, users []*UserRecord) error {
    tx, err := repo.NewTransaction(nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    records := db.ToAnySlice(users)
    if err := tx.Insert(records...); err != nil {
        return err
    }

    return tx.Commit()
}

Performance Notes: - Batch inserts generate a single optimized SQL statement - Use chunking for very large datasets (>1000 records) - Consider transactions for atomicity - Monitor memory usage with large batches

InsertReturning

func (r *repository) InsertReturning(record any, returnFields []string, target ...any) error

Inserts a record and returns specified fields. Supports three scanning modes:

  1. Struct scanning - Maps returned fields to struct by name/tag
  2. Positional scanning - Maps to multiple variables by position
  3. Single value - Returns a single field value

Examples:

// Struct scanning - returns all fields into struct
user := &UserRecord{Name: "John", Email: "john@example.com"}
result := &UserRecord{}
err := repo.InsertReturning(user, []string{"id", "name", "email", "created_at"}, result)

// Positional scanning - returns to individual variables
var id int64
var createdAt time.Time
err := repo.InsertReturning(user, []string{"id", "created_at"}, &id, &createdAt)

// Single value - returns just the ID
var newID int64
err := repo.InsertReturning(user, []string{"id"}, &newID)

Updater Interface

The Updater interface provides comprehensive update operations:

type Updater interface {
    Update(qry *goqu.UpdateDataset) error
    UpdateReturning(record any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
    UpdateRecord(record any, whereFieldsValues map[string]any) error
    UpdateFields(record any, fieldsValues map[string]any, whereFieldsValues map[string]any) error
    UpdateFieldsReturning(record any, fieldsValues map[string]any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
    UpdateByKey(record any, keyField string, value any) error
}

Update (Deprecated)

func (r *repository) Update(qry *goqu.UpdateDataset) error

Executes an update using goqu UpdateDataset. This method is deprecated due to serialization issues with some data types. Use UpdateRecord instead.

UpdateRecord

func (r *repository) UpdateRecord(record any, whereFieldsValues map[string]any) error

Updates a record using the modern query builder. WHERE conditions are combined with AND.

Example:

user := &UserRecord{
    Name:      "John Updated",
    Email:     "john.updated@example.com",
    UpdatedAt: time.Now(),
}

err := repo.UpdateRecord(user, map[string]any{"id": 123})

UpdateFields

func (r *repository) UpdateFields(record any, fieldsValues map[string]any, whereFieldsValues map[string]any) error

Updates specific fields only, useful for partial updates.

Example:

// Update only specific fields
err := repo.UpdateFields(
    &UserRecord{}, // empty struct for type info
    map[string]any{
        "last_login": time.Now(),
        "login_count": goqu.L("login_count + 1"),
    },
    map[string]any{"id": 123},
)

UpdateReturning

func (r *repository) UpdateReturning(record any, whereFieldsValues map[string]any, returnFields []string, target ...any) error

Updates a record and returns specified fields. Supports the same three scanning modes as InsertReturning.

Example:

user := &UserRecord{Name: "John Updated"}
result := &UserRecord{}

err := repo.UpdateReturning(
    user,
    map[string]any{"id": 123},
    []string{"id", "name", "updated_at"},
    result,
)

UpdateFieldsReturning

func (r *repository) UpdateFieldsReturning(record any, fieldsValues map[string]any, whereFieldsValues map[string]any, returnFields []string, target ...any) error

Updates specific fields and returns values. Combines UpdateFields with RETURNING support.

Example:

var updatedAt time.Time
err := repo.UpdateFieldsReturning(
    &UserRecord{},
    map[string]any{"status": "verified"},
    map[string]any{"id": 123},
    []string{"updated_at"},
    &updatedAt,
)

UpdateByKey

func (r *repository) UpdateByKey(record any, keyField string, value any) error

Updates a record using a single key field condition.

Example:

user := &UserRecord{
    Name:      "Updated Name",
    UpdatedAt: time.Now(),
}

err := repo.UpdateByKey(user, "id", 123)

Deleter Interface

The Deleter interface provides methods for deleting records:

type Deleter interface {
    Delete(qry *goqu.DeleteDataset) error
    DeleteWhere(fieldNameValue map[string]any) error
    DeleteByKey(keyField string, value any) error
}

Delete

func (r *repository) Delete(qry *goqu.DeleteDataset) error

Executes a delete query using goqu DeleteDataset.

Example:

err := repo.Delete(
    repo.SqlDelete().Where(
        goqu.C("created_at").Lt(time.Now().AddDate(-1, 0, 0)),
    ),
)

DeleteWhere

func (r *repository) DeleteWhere(fieldNameValue map[string]any) error

Deletes records matching field values. All conditions are combined with AND.

Example:

err := repo.DeleteWhere(map[string]any{
    "status": "inactive",
    "verified": false,
})

DeleteByKey

func (r *repository) DeleteByKey(keyField string, value any) error

Deletes a record by a single key field.

Example:

err := repo.DeleteByKey("id", 123)

GridOps Interface

The Repository also implements a GridOps interface that provides methods for working with data grids:

type GridOps interface {
    Grid(record any) (*Grid, error)
    QueryGrid(record any, args GridQuery, dest any) error
}
  • Grid(record any) (*Grid, error) - Creates a Grid object based on the provided record type, using field tags to determine which fields can be sorted, filtered, or searched
  • QueryGrid(record any, args GridQuery, dest any) error - Creates a Grid object and executes a query using the provided GridQuery parameters

Example Usage

type UserRecord struct {
    ID       int    `db:"id" json:"id" grid:"sort,filter"`
    Username string `db:"username" json:"username" grid:"sort,search,filter"`
    Email    string `db:"email" json:"email" grid:"sort,search,filter"`
    Active   bool   `db:"active" json:"active" grid:"filter"`
}

func main() {
    // ... setup connection and repository as shown earlier ...
    repo := db.NewRepository(context.Background(), client, "users")

    // Create a GridQuery for searching and filtering
    query, err := db.NewGridQuery(db.SearchAny, 10, 0)
    if err != nil {
        log.Fatal(err)
    }

    // Set search parameters
    query.SearchText = "john"
    query.FilterFields = map[string]any{
        "active": true,
    }
    query.SortFields = map[string]string{
        "username": db.SortAscending,
    }

    // Execute the query
    var users []*UserRecord
    if err := repo.QueryGrid(&UserRecord{}, query, &users); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Found %d active users matching 'john'\n", len(users))

    // Alternatively, create and configure a Grid manually
    grid, err := repo.Grid(&UserRecord{})
    if err != nil {
        log.Fatal(err)
    }

    // Add custom filter function
    grid.AddFilterFunc("active", func(value any) (any, error) {
        switch v := value.(type) {
        case string:
            switch v {
            case "yes", "true", "1":
                return true, nil
            case "no", "false", "0":
                return false, nil
            default:
                return nil, fmt.Errorf("invalid boolean value: %v", v)
            }
        case bool:
            return v, nil
        default:
            return nil, fmt.Errorf("unsupported type: %T", value)
        }
    })

    // Validate and build the query
    if err := grid.ValidQuery(query); err != nil {
        log.Fatal(err)
    }

    statement, err := grid.Build(repo.SqlSelect(), query)
    if err != nil {
        log.Fatal(err)
    }

    // Execute the query
    var filteredUsers []*UserRecord
    if err := repo.Fetch(statement, &filteredUsers); err != nil {
        log.Fatal(err)
    }
}

For more detailed information about grid functionality, see the Data Grid documentation.

Builder Interface

The Builder interface provides methods for creating SQL query builders:

type Builder interface {
    Sql() goqu.DialectWrapper
    SqlSelect() *goqu.SelectDataset
    SqlInsert() *goqu.InsertDataset
    SqlUpdate() *goqu.UpdateDataset
    SqlDelete() *goqu.DeleteDataset
}

Sql

func (r *repository) Sql() goqu.DialectWrapper

Returns the goqu dialect wrapper for building custom queries.

Example:

dialect := repo.Sql()
customQuery := dialect.From("users").
    InnerJoin(goqu.T("departments"), goqu.On(goqu.C("users.dept_id").Eq(goqu.C("departments.id")))).
    Select("users.*", "departments.name")

SqlSelect

func (r *repository) SqlSelect() *goqu.SelectDataset

Returns a SELECT query builder for the repository's table.

Example:

query := repo.SqlSelect().
    Where(goqu.C("active").IsTrue()).
    OrderBy(goqu.C("created_at").Desc()).
    Limit(10)

var users []*UserRecord
err := repo.Fetch(query, &users)

SqlInsert

func (r *repository) SqlInsert() *goqu.InsertDataset

Returns an INSERT query builder. Note: goqu prepared statements are not compatible with PostgreSQL extended types; use Insert() method or SqlBuilder() instead.

Example:

// Not recommended for PostgreSQL extended types
insertQuery := repo.SqlInsert().Rows(
    goqu.Record{"name": "John", "email": "john@example.com"},
)

SqlUpdate

func (r *repository) SqlUpdate() *goqu.UpdateDataset

Returns an UPDATE query builder. Note: goqu prepared statements are not compatible with PostgreSQL extended types; use UpdateRecord() or SqlBuilder() instead.

SqlDelete

func (r *repository) SqlDelete() *goqu.DeleteDataset

Returns a DELETE query builder for the repository's table.

Example:

deleteQuery := repo.SqlDelete().
    Where(goqu.C("status").Eq("deleted")).
    Where(goqu.C("deleted_at").Lt(time.Now().AddDate(0, -1, 0)))

err := repo.Delete(deleteQuery)

SqlBuilder Interface

The SqlBuilder interface provides access to the modern query builder:

type SqlBuilder interface {
    SqlDialect() qb.SqlDialect
    SqlBuilder() *qb.SqlBuilder
    SqlUpdateX(record any) *qb.UpdateBuilder
    Do(qry any, target ...any) error
}

SqlDialect

func (r *repository) SqlDialect() qb.SqlDialect

Returns the SQL dialect being used by the repository.

Example:

dialect := repo.SqlDialect()
log.Printf("Using dialect: %s", dialect.Name())

SqlBuilder

func (r *repository) SqlBuilder() *qb.SqlBuilder

Returns the query builder instance for advanced SQL construction.

Example:

builder := repo.SqlBuilder()
// Use builder for complex operations

SqlUpdateX

func (r *repository) SqlUpdateX(record any) *qb.UpdateBuilder

Creates an UpdateBuilder for the given record with advanced options.

Example:

user := &UserRecord{Name: "Updated Name"}
updateBuilder := repo.SqlUpdateX(user).
    WithOptions(&qb.UpdateOptions{
        IncludeFields: []string{"name", "updated_at"},
        UpdateAutoFields: true,
    }).
    Where(qb.Eq("id", 123))

err := repo.Do(updateBuilder)

Do

func (r *repository) Do(qry any, target ...any) error

Executes various query types (SELECT, UPDATE, INSERT, DELETE) with optional target for results.

Example:

// Execute UpdateBuilder
updateBuilder := repo.SqlUpdateX(user).Where(qb.Eq("id", 123))
err := repo.Do(updateBuilder)

// Execute with RETURNING
updateBuilder = repo.SqlUpdateX(user).
    WithOptions(&qb.UpdateOptions{
        ReturningFields: []string{"id", "updated_at"},
    }).
    Where(qb.Eq("id", 123))

result := &UserRecord{}
err := repo.Do(updateBuilder, result)

Transaction Support

The Repository supports database transactions through the Transaction interface:

type Transaction interface {
    Builder
    Reader
    Executor
    Writer
    Deleter
    Updater
    Counter
    SqlBuilder
    Db() *sqlx.Tx
    Name() string
    Commit() error
    Rollback() error
}

Creating Transactions

func (r *repository) NewTransaction(opts *sql.TxOptions) (Transaction, error)

Creates a new transaction with optional transaction options.

Example:

// Basic transaction
tx, err := repo.NewTransaction(nil)
if err != nil {
    return err
}
defer tx.Rollback() // Always defer rollback

// Transaction with options
opts := &sql.TxOptions{
    Isolation: sql.LevelSerializable,
    ReadOnly:  false,
}
tx, err := repo.NewTransaction(opts)

Using Transactions

Transactions implement all the same interfaces as Repository, allowing seamless operation:

func transferFunds(repo db.Repository, fromID, toID int, amount decimal.Decimal) error {
    tx, err := repo.NewTransaction(nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // Debit source account
    err = tx.UpdateFields(
        &AccountRecord{},
        map[string]any{"balance": goqu.L("balance - ?", amount)},
        map[string]any{"id": fromID},
    )
    if err != nil {
        return fmt.Errorf("failed to debit account: %w", err)
    }

    // Credit destination account
    err = tx.UpdateFields(
        &AccountRecord{},
        map[string]any{"balance": goqu.L("balance + ?", amount)},
        map[string]any{"id": toID},
    )
    if err != nil {
        return fmt.Errorf("failed to credit account: %w", err)
    }

    // Insert transaction record
    txRecord := &TransactionRecord{
        FromAccountID: fromID,
        ToAccountID:   toID,
        Amount:        amount,
        CreatedAt:     time.Now(),
    }
    if err := tx.Insert(txRecord); err != nil {
        return fmt.Errorf("failed to record transaction: %w", err)
    }

    // Commit the transaction
    return tx.Commit()
}

Transaction Best Practices

  1. Always defer Rollback(): Even if you plan to commit, deferred rollback is a safety net
  2. Keep transactions short: Long-running transactions can cause lock contention
  3. Handle errors properly: Any error should trigger a rollback
  4. Use appropriate isolation levels: Choose based on your consistency requirements

Example of complex transaction:

func processOrder(repo db.Repository, orderID int) error {
    tx, err := repo.NewTransaction(&sql.TxOptions{
        Isolation: sql.LevelReadCommitted,
    })
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // Lock order for update
    var order OrderRecord
    err = tx.FetchRecord(
        map[string]any{"id": orderID, "status": "pending"},
        &order,
    )
    if err != nil {
        if db.EmptyResult(err) {
            return errors.New("order not found or already processed")
        }
        return err
    }

    // Process order items
    var items []*OrderItemRecord
    err = tx.FetchWhere(map[string]any{"order_id": orderID}, &items)
    if err != nil {
        return err
    }

    for _, item := range items {
        // Update inventory
        err = tx.UpdateFields(
            &ProductRecord{},
            map[string]any{"stock": goqu.L("stock - ?", item.Quantity)},
            map[string]any{"id": item.ProductID},
        )
        if err != nil {
            return fmt.Errorf("failed to update inventory: %w", err)
        }
    }

    // Update order status
    order.Status = "completed"
    order.CompletedAt = time.Now()
    err = tx.UpdateRecord(&order, map[string]any{"id": orderID})
    if err != nil {
        return err
    }

    return tx.Commit()
}

Advanced Usage Patterns

Custom Query Execution

func customAggregation(repo db.Repository) error {
    // Complex aggregation query
    query := repo.SqlSelect().
        Select(
            goqu.C("department"),
            goqu.COUNT("*").As("count"),
            goqu.AVG("salary").As("avg_salary"),
            goqu.MAX("salary").As("max_salary"),
        ).
        GroupBy("department").
        Having(goqu.COUNT("*").Gt(5)).
        OrderBy(goqu.C("avg_salary").Desc())

    var results []struct {
        Department string  `db:"department"`
        Count      int     `db:"count"`
        AvgSalary  float64 `db:"avg_salary"`
        MaxSalary  float64 `db:"max_salary"`
    }

    return repo.Fetch(query, &results)
}

Batch Insert Operations

func batchInsertWithProgress(repo db.Repository, users []*UserRecord) error {
    const batchSize = 500
    total := len(users)

    for i := 0; i < total; i += batchSize {
        end := i + batchSize
        if end > total {
            end = total
        }

        batch := users[i:end]
        records := db.ToAnySlice(batch)

        err := repo.Insert(records...)
        if err != nil {
            return fmt.Errorf("batch %d-%d failed: %w", i, end-1, err)
        }

        progress := float64(end) / float64(total) * 100
        log.Printf("Inserted %d-%d: %.1f%% complete", i, end-1, progress)
    }

    return nil
}

func batchInsertWithErrorRecovery(repo db.Repository, users []*UserRecord) error {
    var successful []string
    var failed []struct {
        User  *UserRecord
        Error error
    }

    // Try batch insert first
    records := db.ToAnySlice(users)
    err := repo.Insert(records...)
    if err == nil {
        log.Printf("Successfully batch inserted %d users", len(users))
        return nil
    }

    log.Printf("Batch insert failed, trying individual inserts: %v", err)

    // Fall back to individual inserts
    for _, user := range users {
        err := repo.Insert(user)
        if err != nil {
            failed = append(failed, struct {
                User  *UserRecord
                Error error
            }{user, err})
        } else {
            successful = append(successful, user.Name)
        }
    }

    log.Printf("Individual inserts: %d successful, %d failed", len(successful), len(failed))

    if len(failed) > 0 {
        return fmt.Errorf("failed to insert %d records", len(failed))
    }

    return nil
}

Batch Update Operations

func batchUpdateWithProgress(repo db.Repository, updates []UserUpdate) error {
    total := len(updates)
    completed := 0

    for i := 0; i < total; i += 100 {
        end := i + 100
        if end > total {
            end = total
        }

        tx, err := repo.NewTransaction(nil)
        if err != nil {
            return err
        }

        for _, update := range updates[i:end] {
            err := tx.UpdateByKey(&update, "id", update.ID)
            if err != nil {
                tx.Rollback()
                return fmt.Errorf("failed at record %d: %w", update.ID, err)
            }
        }

        if err := tx.Commit(); err != nil {
            return err
        }

        completed = end
        log.Printf("Progress: %d/%d (%.1f%%)", completed, total, float64(completed)/float64(total)*100)
    }

    return nil
}

Repository Factory Pattern

type RepositoryFactory struct {
    client db.Client
    ctx    context.Context
}

func NewRepositoryFactory(client db.Client) *RepositoryFactory {
    return &RepositoryFactory{
        client: client,
        ctx:    context.Background(),
    }
}

func (f *RepositoryFactory) Users() db.Repository {
    return db.NewRepository(f.ctx, f.client, "users")
}

func (f *RepositoryFactory) Orders() db.Repository {
    return db.NewRepository(f.ctx, f.client, "orders")
}

func (f *RepositoryFactory) Products() db.Repository {
    return db.NewRepository(f.ctx, f.client, "products")
}

// Usage
factory := NewRepositoryFactory(client)
userRepo := factory.Users()
orderRepo := factory.Orders()

Performance Considerations

Connection Pooling

  • Repositories share the client's connection pool
  • Configure pool size based on concurrent operations
  • Monitor pool usage to avoid exhaustion

Query Optimization

  • Use appropriate indexes for WHERE clauses
  • Batch operations when possible
  • Use RETURNING clauses to avoid extra queries
  • Consider pagination for large result sets

Caching

  • Repository caches field specifications per struct type
  • Grid specifications are cached after first use
  • Consider application-level caching for frequently accessed data

Error Handling

The Repository provides consistent error handling:

// Check for empty results
user := &UserRecord{}
err := repo.FetchByKey("id", 123, user)
if err != nil {
    if db.EmptyResult(err) {
        // Handle not found case
        return nil, ErrUserNotFound
    }
    // Handle actual error
    return nil, fmt.Errorf("database error: %w", err)
}

// Handle constraint violations
err = repo.Insert(user)
if err != nil {
    if isUniqueViolation(err) {
        return ErrDuplicateEmail
    }
    return fmt.Errorf("insert failed: %w", err)
}

Best Practices

  1. Use appropriate interfaces: Don't pass full Repository when only Reader is needed
  2. Leverage transactions: Group related operations in transactions
  3. Handle errors properly: Always check for EmptyResult on single record fetches
  4. Use struct tags: Properly tag structs for field mapping and grid functionality
  5. Batch operations: Use batch methods for bulk operations
  6. Monitor performance: Log slow queries and optimize as needed

See Also