Seederek
A migrációk a sémát hozzák létre; a seederek az adatot: egy admin usert, szerepköröket, lookup táblákat. A gpsystem ehhez a seed csomagot és egy központi seeds/ package-et generál minden projektbe, a cmd/migrate binárison keresztül futtatva.
go run ./cmd/migrate up --seed # migrál, majd seedel
go run ./cmd/migrate seed # csak seedel, migráció nélkül
A Registry és a Seeder
Egy seeder egy seed.Seeder érték: név, opcionális Guard (eldönti, lefusson-e egyáltalán) és a Run törzs. Az add seeder parancs generálja és regisztrálja a seeds/ package-ben:
func adminUser(deps Deps) seed.Seeder {
return seed.Seeder{
Name: "admin_user",
Run: func(ctx context.Context) error {
return nil
},
}
}
func Register(reg *seed.Registry, deps Deps) {
reg.Add(adminUser(deps))
// gpsystem:seeds
}
A Deps a modulok Dependencies-ét tükrözi: a query-felület (*pg.DB vagy *bun.DB, a projekt --db választása szerint) és a dbx.Transactor. A Registry.Run regisztrálási sorrendben futtatja a seedereket: számít, ha az egyik egy másik által létrehozott sorra hivatkozik (pl. a szerepkörök az admin user előtt).
A lelke: first-or-create
A seederek idempotensek: kétszeri futtatás nem duplikál. A bun-alapú projektekhez ehhez van egy generikus primitív, a seed/bunx.FirstOrCreate:
user := &User{Email: "admin@example.com", Name: "Admin"}
created, err := bunx.FirstOrCreate(ctx, deps.DB, user, "email")
Megkeresi az email oszlop alapján a meglévő sort; ha van, betölti user-be (a mezők a valódi sor értékeire állnak), ha nincs, beszúrja user-t úgy, ahogy van. A bunx.From-on át fut, tehát csatlakozik egy WithinTransaction-nal nyitott tranzakcióhoz, ha van ilyen. Összetettebb egyezésvizsgálathoz (nem sima oszlop-egyenlőség) ott a FirstOrCreateWhere, ami egy tetszőleges *bun.SelectQuery lezárást vár.
Natív (pgx) projektekhez nincs kit-oldali helper: a seeder saját INSERT ... ON CONFLICT (oszlop) DO NOTHING SQL-t ír a pg.DBTX-en, ugyanúgy, ahogy bármelyik más natív repository.
Feltételes futás: Guard és ErrSkip
Egy seedernek el kell tudnia dönteni, hogy egyáltalán lefusson-e: erre való a Guard. Kiértékelése megelőzi a Run-t; false esetén a runner skipnek logolja, nem hibának:
seed.Seeder{
Name: "demo_data",
Guard: seed.OnlyEnv(currentEnv, "dev", "staging"),
Run: loadDemoData,
}
Amikor a feltétel csak futásidőben derül ki (pl. a tenant tartalmától függ), a Run maga is kiszállhat: a seed.ErrSkip visszaadása ugyanúgy skipnek számít, nem hibának.
Run: func(ctx context.Context) error {
t, _ := seed.TenantFromContext(ctx)
if t.Data.(TenantConfig).Plan != "enterprise" {
return seed.ErrSkip
}
_, err := bunx.FirstOrCreate(ctx, deps.DB, &EnterpriseDefaults{TenantID: t.ID}, "tenant_id")
return err
},
Multi-tenant: eltérő tartalom tenantonként
Egy seeder ugyanaz a kód lehet minden tenantra, miközben más-más adatot hoz létre: a Registry.Run a seed.WithTenants opcióval tenantonként fut, a tenantot a contextbe téve.
tenants := []seed.Tenant{
{ID: "acme", Data: TenantConfig{Plan: "enterprise"}},
{ID: "globex", Data: TenantConfig{Plan: "starter"}},
}
if err := reg.Run(ctx, seed.WithTenants(tenants)); err != nil {
log.Fatal(err)
}
A Tenant.Data tetszőleges, projekt-specifikus payload; a seeder típuskonverzióval (type assertion) éri el. A seed.OnlyTenants(ids...) egy kész Guard arra az esetre, ha egy seeder csak bizonyos tenantokra fusson.
search_path-váltás, nincs RLS): a Tenant-lista honnan jön (env, egy tábla, egy admin API) a projektre van bízva. A WithTenants egy kampó, nem egy kész multi-tenant infrastruktúra.Miért nem a migrációk része?
A migráció és a seedelés más-más életciklust futhat: egy migráció mindig lefut, egy seeder gyakran csak fejlesztésben vagy első telepítéskor kell. Ezért a --seed flag és a seed subcommand explicit: go run ./cmd/migrate up önmagában sosem ír adatot, csak sémát. Éles környezetben ez azt jelenti, hogy a seedelés bekapcsolása (vagy kihagyása) a deploy-pipeline döntése, nem a migrációs lépés hallgatólagos mellékhatása.