add seeder
Seedert akkor érdemes elővenni, amikor a projektnek a migrációk lefutása után, de még bármilyen felhasználói művelet előtt kell léteznie valamilyen alapadatnak: egy admin fiók, amivel be lehet lépni, egy fix szerepkör- vagy lookup-lista, amire egy idegen kulcs hivatkozik, demo adat egy staging környezethez. A felhasználói művelet hatására létrejövő üzleti sorok helye egy service, nem ez; a seeder azokra a sorokra való, amikről az app feltételezi, hogy már ott vannak.
go tool gpsystem add seeder <név>
Egy seeder-stubot ír a projekt központi seeds/ package-ébe, és regisztrálja a seeds/register.go-ban. A seeder egy seed.Seeder-t ad vissza: nevet, opcionális Guardot (eldönti, lefusson-e egyáltalán) és a Run törzset. Részletek és minták: Seederek.
go tool gpsystem add seeder adminUser
Mit generál
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 seeds/register.go-ban, new project-től kezdve minden projektben ott van) a projekt --db választása szerint *pg.DB vagy *bun.DB mezőt hordoz, plusz egy dbx.Transactor-t. A parancs a new project óta minden projektben meglévő seeds/register.go-t várja; ha hiányzik, hibázik, és jelzi, hogy hiányzik.
Egy kidolgozott, több sort érintő példa
A seederek regisztrációs sorrendben futnak (az Add megőrzi a sorrendet), ez pedig számít, mihelyt az egyik seeder sorai egy másik idegen kulcsát adják: egy szerepkörkészletnek léteznie kell, mielőtt az admin user, ami hivatkozik rá, létrejön. Két seeder, ebben a sorrendben hozzáadva:
go tool gpsystem add seeder roles
go tool gpsystem add seeder adminUser
func roles(deps Deps) seed.Seeder {
names := []string{"admin", "editor", "viewer"}
return seed.Seeder{
Name: "roles",
Run: func(ctx context.Context) error {
for _, name := range names {
if _, err := deps.DB.NewInsert().
Model(&Role{Name: name}).
On("CONFLICT (name) DO NOTHING").
Exec(ctx); err != nil {
return fmt.Errorf("seed role %q: %w", name, err)
}
}
return nil
},
}
}
func adminUser(deps Deps) seed.Seeder {
return seed.Seeder{
Name: "admin_user",
Guard: func(ctx context.Context) (bool, error) {
exists, err := deps.DB.NewSelect().Model((*User)(nil)).
Where("email = ?", "admin@example.com").Exists(ctx)
return !exists, err
},
Run: func(ctx context.Context) error {
hash, err := bcrypt.GenerateFromPassword([]byte(mustEnv("SEED_ADMIN_PASSWORD")), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = deps.DB.NewInsert().Model(&User{
Email: "admin@example.com",
PasswordHash: string(hash),
Role: "admin",
}).Exec(ctx)
return err
},
}
}
func Register(reg *seed.Registry, deps Deps) {
reg.Add(roles(deps))
reg.Add(adminUser(deps))
// gpsystem:seeds
}
A roles az ON CONFLICT DO NOTHING-ot használja, hogy önmagában is idempotens maradjon ismételt futtatásnál; az adminUser Guardja ugyanezt a célt éri el úgy, hogy előbb megnézi, létezik-e már a sor, így a futás tiszta skipként naplózódik egy duplikált kulcs miatti hiba helyett a második migrate up --seed-nél. Mindkettő helyes megoldás: azt válaszd, amelyik jobban illik az adott adathoz.
Futtatás
go run ./cmd/migrate up --seed # migrál, majd seedel
go run ./cmd/migrate seed # csak seedel
add seeder önmagában csak a stubot írja meg és regisztrálja: az üzleti logikát (mit tölts be, milyen feltétellel) a generált fájlba magadnak kell megírnod. Lásd a Seederek oldalt a Guard, ErrSkip és a multi-tenant WithTenants mintákért.