Files
whatshooked/tooldoc/BUN_ORM.md
Hein f9773bd07f
Some checks failed
CI / Test (1.23) (push) Failing after -22m46s
CI / Test (1.22) (push) Failing after -22m32s
CI / Build (push) Failing after -23m30s
CI / Lint (push) Failing after -23m12s
refactor(API): Relspect integration
2026-02-05 13:39:43 +02:00

10 KiB

BUN ORM Integration Guide

Overview

BUN is a fast and lightweight SQL-first ORM for Go. For WhatsHooked Phase 2, we use BUN with PostgreSQL and SQLite, integrated with ResolveSpec for REST API generation.

Official Documentation: https://bun.uptrace.dev/

Installation

go get github.com/uptrace/bun
go get github.com/uptrace/bun/driver/pgdriver     # PostgreSQL
go get github.com/uptrace/bun/driver/sqliteshim   # SQLite
go get github.com/uptrace/bun/dialect/pgdialect
go get github.com/uptrace/bun/dialect/sqlitedialect

Setup

Database Connection

import (
    "database/sql"
    "github.com/uptrace/bun"
    "github.com/uptrace/bun/driver/pgdriver"
    "github.com/uptrace/bun/dialect/pgdialect"
)

// PostgreSQL
func NewPostgresDB(dsn string) *bun.DB {
    sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
    db := bun.NewDB(sqldb, pgdialect.New())
    return db
}

// SQLite
import (
    "github.com/uptrace/bun/driver/sqliteshim"
    "github.com/uptrace/bun/dialect/sqlitedialect"
)

func NewSQLiteDB(path string) *bun.DB {
    sqldb, _ := sql.Open(sqliteshim.ShimName, path)
    db := bun.NewDB(sqldb, sqlitedialect.New())
    return db
}

Model Definition

BUN models use struct tags:

type User struct {
    bun.BaseModel `bun:"table:users,alias:u"`
    
    ID        string    `bun:"id,pk,type:varchar(36)"`
    Username  string    `bun:"username,unique,notnull"`
    Email     string    `bun:"email,unique,notnull"`
    Password  string    `bun:"password,notnull"`
    Role      string    `bun:"role,notnull,default:'user'"`
    Active    bool      `bun:"active,notnull,default:true"`
    CreatedAt time.Time `bun:"created_at,notnull,default:now()"`
    UpdatedAt time.Time `bun:"updated_at,notnull,default:now()"`
    DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
    
    // Relationships
    APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"`
}

CRUD Operations

Create

user := &User{
    ID:       uuid.New().String(),
    Username: "john",
    Email:    "john@example.com",
    Password: hashedPassword,
    Role:     "user",
    Active:   true,
}

_, err := db.NewInsert().
    Model(user).
    Exec(ctx)

Read

// Single record
user := new(User)
err := db.NewSelect().
    Model(user).
    Where("id = ?", userID).
    Scan(ctx)

// Multiple records
var users []User
err := db.NewSelect().
    Model(&users).
    Where("active = ?", true).
    Order("created_at DESC").
    Limit(10).
    Scan(ctx)

Update

// Update specific fields
_, err := db.NewUpdate().
    Model(&user).
    Column("username", "email").
    Where("id = ?", user.ID).
    Exec(ctx)

// Update all fields
_, err := db.NewUpdate().
    Model(&user).
    WherePK().
    Exec(ctx)

Delete

// Soft delete (if model has soft_delete tag)
_, err := db.NewDelete().
    Model(&user).
    Where("id = ?", userID).
    Exec(ctx)

// Hard delete
_, err := db.NewDelete().
    Model(&user).
    Where("id = ?", userID).
    ForceDelete().
    Exec(ctx)

Relationships

Has-Many

type User struct {
    bun.BaseModel `bun:"table:users"`
    ID      string     `bun:"id,pk"`
    APIKeys []*APIKey  `bun:"rel:has-many,join:id=user_id"`
}

type APIKey struct {
    bun.BaseModel `bun:"table:api_keys"`
    ID     string `bun:"id,pk"`
    UserID string `bun:"user_id,notnull"`
    User   *User  `bun:"rel:belongs-to,join:user_id=id"`
}

// Load with relations
var users []User
err := db.NewSelect().
    Model(&users).
    Relation("APIKeys").
    Scan(ctx)

Belongs-To

var apiKey APIKey
err := db.NewSelect().
    Model(&apiKey).
    Relation("User").
    Where("api_key.id = ?", keyID).
    Scan(ctx)

Many-to-Many

type User struct {
    ID    string  `bun:"id,pk"`
    Roles []*Role `bun:"m2m:user_roles,join:User=Role"`
}

type Role struct {
    ID    string  `bun:"id,pk"`
    Users []*User `bun:"m2m:user_roles,join:Role=User"`
}

type UserRole struct {
    UserID string `bun:"user_id,pk"`
    RoleID string `bun:"role_id,pk"`
    User   *User  `bun:"rel:belongs-to,join:user_id=id"`
    Role   *Role  `bun:"rel:belongs-to,join:role_id=id"`
}

Queries

Filtering

// WHERE clauses
err := db.NewSelect().
    Model(&users).
    Where("role = ?", "admin").
    Where("active = ?", true).
    Scan(ctx)

// OR conditions
err := db.NewSelect().
    Model(&users).
    WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
        return q.
            Where("role = ?", "admin").
            Where("role = ?", "moderator")
    }).
    Scan(ctx)

// IN clause
err := db.NewSelect().
    Model(&users).
    Where("role IN (?)", bun.In([]string{"admin", "moderator"})).
    Scan(ctx)

// LIKE
err := db.NewSelect().
    Model(&users).
    Where("username LIKE ?", "john%").
    Scan(ctx)

Sorting

err := db.NewSelect().
    Model(&users).
    Order("created_at DESC").
    Order("username ASC").
    Scan(ctx)

Pagination

// Offset/Limit
err := db.NewSelect().
    Model(&users).
    Limit(20).
    Offset(40).
    Scan(ctx)

// Count
count, err := db.NewSelect().
    Model((*User)(nil)).
    Where("active = ?", true).
    Count(ctx)

Aggregations

// COUNT
count, err := db.NewSelect().
    Model((*User)(nil)).
    Count(ctx)

// GROUP BY
type Result struct {
    Role  string `bun:"role"`
    Count int    `bun:"count"`
}

var results []Result
err := db.NewSelect().
    Model((*User)(nil)).
    Column("role").
    ColumnExpr("COUNT(*) as count").
    Group("role").
    Scan(ctx, &results)

Transactions

err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
    // Create user
    _, err := tx.NewInsert().
        Model(&user).
        Exec(ctx)
    if err != nil {
        return err
    }
    
    // Create API key
    _, err = tx.NewInsert().
        Model(&apiKey).
        Exec(ctx)
    if err != nil {
        return err
    }
    
    return nil
})

Migrations

Create Table

_, err := db.NewCreateTable().
    Model((*User)(nil)).
    IfNotExists().
    Exec(ctx)

Add Column

_, err := db.NewAddColumn().
    Model((*User)(nil)).
    ColumnExpr("phone VARCHAR(50)").
    Exec(ctx)

Drop Table

_, err := db.NewDropTable().
    Model((*User)(nil)).
    IfExists().
    Exec(ctx)

Hooks

Before/After Hooks

var _ bun.BeforeAppendModelHook = (*User)(nil)

func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
    switch query.(type) {
    case *bun.InsertQuery:
        u.CreatedAt = time.Now()
    case *bun.UpdateQuery:
        u.UpdatedAt = time.Now()
    }
    return nil
}

ResolveSpec Integration

Setup with BUN

import (
    "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
    "git.warky.dev/wdevs/whatshooked/pkg/models"
)

// Create ResolveSpec handler with BUN
handler := restheadspec.NewHandlerWithBun(db)

// Setup routes (models are automatically discovered)
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)

Custom Queries

// Use BUN directly for complex queries
var results []struct {
    UserID   string
    HookCount int
}

err := db.NewSelect().
    Model((*models.Hook)(nil)).
    Column("user_id").
    ColumnExpr("COUNT(*) as hook_count").
    Group("user_id").
    Scan(ctx, &results)

Performance Tips

1. Use Column Selection

// Don't load unnecessary columns
err := db.NewSelect().
    Model(&users).
    Column("id", "username", "email").
    Scan(ctx)

2. Batch Operations

// Insert multiple records
_, err := db.NewInsert().
    Model(&users).
    Exec(ctx)

3. Use Indexes

indexes {
    (user_id) [name: 'idx_hooks_user_id']
    (created_at) [name: 'idx_hooks_created_at']
}

4. Connection Pooling

sqldb.SetMaxOpenConns(25)
sqldb.SetMaxIdleConns(25)
sqldb.SetConnMaxLifetime(5 * time.Minute)

5. Prepared Statements

BUN automatically uses prepared statements for better performance.

Common Patterns

Repository Pattern

type UserRepository struct {
    db *bun.DB
}

func NewUserRepository(db *bun.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) GetByID(ctx context.Context, id string) (*models.User, error) {
    user := new(models.User)
    err := r.db.NewSelect().
        Model(user).
        Where("id = ?", id).
        Scan(ctx)
    return user, err
}

func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) {
    user := new(models.User)
    err := r.db.NewSelect().
        Model(user).
        Where("username = ?", username).
        Scan(ctx)
    return user, err
}

Soft Deletes

type User struct {
    bun.BaseModel `bun:"table:users"`
    ID        string       `bun:"id,pk"`
    DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
}

// Automatically filters out soft-deleted records
var users []User
err := db.NewSelect().
    Model(&users).
    Scan(ctx)

// Include soft-deleted records
err := db.NewSelect().
    Model(&users).
    WhereAllWithDeleted().
    Scan(ctx)

Multi-Tenancy

// Filter by user_id for all queries
err := db.NewSelect().
    Model(&hooks).
    Where("user_id = ?", currentUserID).
    Scan(ctx)

Error Handling

import "github.com/uptrace/bun/driver/pgdriver"

err := db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx)

switch {
case errors.Is(err, sql.ErrNoRows):
    // Not found
case err != nil:
    var pgErr pgdriver.Error
    if errors.As(err, &pgErr) {
        // PostgreSQL specific error
        if pgErr.IntegrityViolation() {
            // Handle constraint violation
        }
    }
}

Testing

import (
    "github.com/uptrace/bun"
    "github.com/uptrace/bun/dbfixture"
)

func TestUserRepository(t *testing.T) {
    // Use in-memory SQLite for tests
    db := NewSQLiteDB(":memory:")
    defer db.Close()
    
    // Create tables
    ctx := context.Background()
    _, err := db.NewCreateTable().
        Model((*models.User)(nil)).
        Exec(ctx)
    require.NoError(t, err)
    
    // Test repository
    repo := NewUserRepository(db)
    user := &models.User{...}
    err = repo.Create(ctx, user)
    require.NoError(t, err)
}

References