refactor(API): ✨ Relspect integration
This commit is contained in:
557
tooldoc/BUN_ORM.md
Normal file
557
tooldoc/BUN_ORM.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user