dbx

bun

Az opcionális bun adapter: query builder és struct-modellek, a Transactor kontrakt változatlanul.

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.

Az „egy tranzakció, két query-stílus" garanciát (bun builder + raw SQL, közös commit/rollback) a dbx saját integrációs tesztjei rögzítik valós PostgreSQL ellen: 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.

main.go
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.
Copyright © 2026