bun
A dbx/bunx a dbx tranzakció-modelljének opcionális bun adaptere. A default dbx/pg natív pgx.Tx-et hordoz a contextben és kézzel írt SQL-lel dolgozik; egy projekt, amelyik query buildingre bunt szeretne, ehelyett ezt a csomagot köti be. A bun csak akkor kerül a binárisba, ha valaki importálja a bunx-et. Aki nem igényli, az teljesen bun-mentes marad.
import "github.com/gp-system/dbx/bunx"
Mi a bun, és mi nem
Legyünk őszinték a pozicionálással: a bun egy query builder struct-mapping-gel („ORM-lite"), nem Eloquent. Amit megkapsz:
- struct-alapú modellek (
bun:"..."tagek), builder-stílusú query-írás (NewSelect().Model(...).Where(...)), - relációk betöltése (
Relation(...)), bulk insert/update, típusos scanelés.
Amit nem kapsz meg az Eloquentből: nincs ActiveRecord ($product->save()), nincsenek modell-események (creating/saved), nincs lazy loading, nincsenek accessorok/mutatorok, nincs global scope. A modell egy sima struct, a query egy explicit builder-hívás. Ez egy teljes ActiveRecord-stílusú ORM-hez képest elsőre spártainak tűnhet. Cserébe minden query ott van, ahol leírtad, és pontosan az fut le, amit látsz.
Bekötés
A projekt-generáláskor a --db bun flag választja ki (lásd new project); a manifest rögzíti, és minden későbbi generátor (new module, add event, add worker) bun-változatú kódot emittál. A generált main.go így drótoz:
pool := pg.MustNewPool(ctx, cfg.DB) // a pool-építés közös a pgx-stackkel
bunDB := bunx.Open(pool)
deps := shop.Dependencies{
DB: bunDB, // *bun.DB, a repositoryk ezt fogják
Transactor: bunx.NewTransactor(bunDB), // pg.NewTransactor(pool) helyett
}
A bunx.Open a kapott pgxpool.Pool-ból épít *bun.DB-t (pgdialect-tel), így a dbx.Config és a pool-konfiguráció újrahasznosul; a pool lezárása zárja az alatta lévő kapcsolatokat is. Az Open beköti a bunotel query hookot is: a bun query builderen át futó query-k ugyanúgy OTel spanokat bocsátanak ki (statement + idő), mint a pgx-út, és megjelennek a trace waterfallban és a Sentryben. A bunx.Conn-on futó nyers SQL megkerüli ezt a hookot (lásd lentebb).
A Transactor kontrakt azonos
A bunx.NewTransactor ugyanazt a dbx.Transactor interfészt adja, mint a dbx/pg: nil-re commit, hibára vagy panicre rollback, beágyazott hívás a külső tranzakcióhoz csatlakozik (lapos nesting, a legkülső hívás commitol). A különbség csak a hordozott típus: pgx.Tx helyett bun.Tx utazik a contextben, a bunx.ContextWithTx / bunx.TxFromContext párral.
Ez azt jelenti, hogy a service-réteged nem változik, ha implementációt váltasz: a Tranzakciók oldal PlaceOrder service-e betűre ugyanaz pgx-en és bunon, csak a main.go bekötése és a repositoryk belseje különbözik.
Repositoryk
A bun query builder metódusai (NewSelect, NewInsert, …) nem kapnak contextet, ezért a repository a query-felületet a contextből oldja fel minden metódus tetején. A bunx.From a contextben utazó bun.Tx-et adja vissza (ha a WithinTransaction nyitott egyet), különben a root *bun.DB-t, mindkettő bun.IDB, így a hívó egységesen használja.
A shop termék-repositoryja bunnal:
package repository
type Product struct {
bun.BaseModel `bun:"table:products"`
ID int64 `bun:"id,pk,autoincrement"`
Name string `bun:"name,notnull"`
PriceCents int64 `bun:"price_cents,notnull"`
Stock int `bun:"stock,notnull"`
CreatedAt time.Time `bun:"created_at,notnull"`
}
type ProductRepository struct {
db *bun.DB
}
func (r *ProductRepository) ListProducts(ctx context.Context, limit int) ([]Product, error) {
var products []Product
err := bunx.From(ctx, r.db).NewSelect().Model(&products).
OrderExpr("created_at DESC, id DESC").
Limit(limit).
Scan(ctx)
return products, err
}
A rendelés-insert, a PlaceOrder tranzakciójából hívva, automatikusan a bun.Tx-en fut:
func (r *OrderRepository) Insert(ctx context.Context, o *Order) error {
_, err := bunx.From(ctx, r.db).NewInsert().Model(o).Exec(ctx)
return err
}
Raw SQL és sqlc: a Conn resolver
Mivel a bun.Tx beágyaz egy *sql.Tx-et, a nyers SQL és a sqlc (database/sql módban) ugyanabba a tranzakcióba csatlakozhat a bunx.Conn-on keresztül. A shop készletcsökkentése: atomikus UPDATE, amit builderrel körülményes lenne kifejezni:
func (r *ProductRepository) DecrementStock(ctx context.Context, productID int64, qty int) error {
res, err := bunx.Conn(ctx, r.db).ExecContext(ctx,
`UPDATE products SET stock = stock - $2 WHERE id = $1 AND stock >= $2`,
productID, qty)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return errs.New("insufficient stock",
errs.Code("insufficient_stock"), errs.Public("A termék elfogyott."))
}
return nil
}
A bunx.Conn szándékosan a nyers *sql.Tx / *sql.DB-t adja vissza, nem a bun wrappert. A bun wrapper minden query-t átformáz a saját placeholder-szintaxisára (?), ami eltörné a natív $N placeholdereket, így a sqlc által generált postgres-SQL is változatlanul fut. Cserébe az ezen az úton futó query-k kihagyják a bun query-hookjait (otel/logging); aki azokat akarja, a bun buildert használja a From-on át.
go test -tags=integration ./dbx/bunx/.Outbox bun alatt
Az outbox store-nak is van bun-adaptere, ugyanezt a splitet tükrözve: a bun-projektek a github.com/gp-system/events/outbox/bunx csomag NewStore(bunDB)-jét kötik be, így az outbox-insert a WithinTransaction által nyitott bun-tranzakcióhoz csatlakozik. A generátor ezt automatikusan így drótozza --db bun projektekben:
Dispatcher: outbox.NewDispatcher(outboxbunx.NewStore(bunDB)),
Korlát
Egy projekten belül a bunx és a pgx-natív executor (a dbx/pg DB-je) nem oszthat egy tranzakciót: a két absztrakció külön úton checkoutolja a kapcsolatokat, és külön context-kulcsban hordozza a tranzakciót. Bun-projektben a nyers SQL a bunx.Conn-on megy, sosem a pg.NewDB(pool)-on. A migrációk ettől függetlenek: mindkét stack alatt ugyanaz a cmd/migrate bináris fut.
Önálló példa
A dbx/bunx-hez sem kell semmi a kitből: az Open, a From és egy transactor önmagában is elég.
package main
import (
"context"
"log"
"github.com/gp-system/dbx"
"github.com/gp-system/dbx/bunx"
"github.com/gp-system/dbx/pg"
)
func main() {
ctx := context.Background()
pool := pg.MustNewPool(ctx, dbx.Config{
Host: "localhost", Port: 5432,
User: "shop", Password: "secret", Database: "shop",
})
defer pool.Close()
bunDB := bunx.Open(pool)
tx := bunx.NewTransactor(bunDB)
err := tx.WithinTransaction(ctx, func(ctx context.Context) error {
_, err := bunx.From(ctx, bunDB).NewUpdate().
Table("products").Set("stock = stock - 1").Where("id = ?", "prod_42").Exec(ctx)
return err
})
if err != nil {
log.Fatal(err)
}
}
Használt patternek
- Unit of Work / tranzakció a contextben, ugyanaz a minta, mint a pgx-stacknél: Design patternek.
- Resolver-függvények (
From(ctx, db),Conn(ctx, db)): ugyanaz a "csatlakozz a contextbeli tranzakcióhoz, ha van" logika, bun-specifikus visszatérési típussal.