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

558 lines
10 KiB
Markdown

# 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
```bash
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
```go
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:
```go
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
```go
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
```go
// 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
```go
// 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
```go
// 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
```go
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
```go
var apiKey APIKey
err := db.NewSelect().
Model(&apiKey).
Relation("User").
Where("api_key.id = ?", keyID).
Scan(ctx)
```
### Many-to-Many
```go
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
```go
// 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
```go
err := db.NewSelect().
Model(&users).
Order("created_at DESC").
Order("username ASC").
Scan(ctx)
```
### Pagination
```go
// 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
```go
// 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
```go
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
```go
_, err := db.NewCreateTable().
Model((*User)(nil)).
IfNotExists().
Exec(ctx)
```
### Add Column
```go
_, err := db.NewAddColumn().
Model((*User)(nil)).
ColumnExpr("phone VARCHAR(50)").
Exec(ctx)
```
### Drop Table
```go
_, err := db.NewDropTable().
Model((*User)(nil)).
IfExists().
Exec(ctx)
```
## Hooks
### Before/After Hooks
```go
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
```go
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
```go
// 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
```go
// Don't load unnecessary columns
err := db.NewSelect().
Model(&users).
Column("id", "username", "email").
Scan(ctx)
```
### 2. Batch Operations
```go
// Insert multiple records
_, err := db.NewInsert().
Model(&users).
Exec(ctx)
```
### 3. Use Indexes
```dbml
indexes {
(user_id) [name: 'idx_hooks_user_id']
(created_at) [name: 'idx_hooks_created_at']
}
```
### 4. Connection Pooling
```go
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
```go
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
```go
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
```go
// Filter by user_id for all queries
err := db.NewSelect().
Model(&hooks).
Where("user_id = ?", currentUserID).
Scan(ctx)
```
## Error Handling
```go
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
```go
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
- BUN Documentation: https://bun.uptrace.dev/
- BUN GitHub: https://github.com/uptrace/bun
- PostgreSQL Driver: https://bun.uptrace.dev/postgres/
- SQLite Driver: https://bun.uptrace.dev/sqlite/
- ResolveSpec: https://github.com/bitechdev/ResolveSpec