Factories & Seeding
Fake users and tasks, seeded with one command
So far every row came from psql, typed by hand. That was fine for a few rows, but it is slow, and you type them all again each time the tables are rebuilt. A factory describes one fake row. A seeder uses factories to fill the database. By the end of this part, one command rebuilds the database and fills it with users and tasks.
CodeStart from the Relationships: Has Many code
git clone https://github.com/krishankumar01/goravel-taskflowcd goravel-taskflowgit checkout part-06cp .env.example .envgo run . artisan key:generatedocker compose up -dgo run . artisan migrateThe user factory
Create a factory for users:
go run . artisan make:factory UserFactory SUCCESS Factory created successfullyIt creates database/factories/user_factory.go with an empty Definition. Fill it in, using gofakeit for the fake values:
package factories
import ( "github.com/brianvoe/gofakeit/v7"
"taskflow/app/facades")
type UserFactory struct {}
// Definition Define the model's default state.func (f *UserFactory) Definition() map[string]any { password, _ := facades.Hash().Make("password")
return map[string]any{ "Name": gofakeit.Name(), "Email": gofakeit.Email(), "Password": password, "Role": "member", }}- The keys are the model’s field names, like
DueAt, not the column names, likedue_at. - Every fake user gets the password
password, hashed with theHashfacade. - The role is the plain string
"member", notmodels.RoleMember. The model will import this package, and Go does not let two packages import each other.
Then run:
go mod tidyThe model finds its factory through a Factory method:
package models
import ( "github.com/goravel/framework/contracts/database/factory" "github.com/goravel/framework/database/orm"
"taskflow/database/factories")
7 collapsed lines
type Role string
const ( RoleManager Role = "manager" RoleMember Role = "member")
type User struct { orm.Model Name string `gorm:"size:255;not null" json:"name"` Email string `gorm:"size:255;unique;not null" json:"email"` Password string `gorm:"size:255;not null" json:"-"` Role Role `gorm:"type:enum('manager','member');default:'member';not null" json:"role"`
CreatedTasks []Task `gorm:"foreignKey:CreatedBy" json:"created_tasks"` AssignedTasks []Task `gorm:"foreignKey:AssignedTo" json:"assigned_tasks"`}
func (u *User) Factory() factory.Factory { return &factories.UserFactory{}}The task factory
go run . artisan make:factory TaskFactory SUCCESS Factory created successfullyFill in its Definition:
package factories
import ( "github.com/brianvoe/gofakeit/v7" "github.com/goravel/framework/support/carbon")
type TaskFactory struct {}
// Definition Define the model's default state.func (f *TaskFactory) Definition() map[string]any { title := gofakeit.RandomString([]string{ "Fix the login bug", "Write the API docs", "Review the pull request", "Plan the next release", "Update the dependencies", "Add tests for tasks", "Clean up old logs", "Reply to support emails", }) status := gofakeit.RandomString([]string{"pending", "in-progress", "done"}) due := carbon.Now().AddDays(gofakeit.Number(1, 30)).StartOfDay().SetHour(9)
return map[string]any{ "Title": title, "Description": gofakeit.HackerPhrase(), "Status": status, "DueAt": carbon.NewDateTime(due), }}Due dates are 1 to 30 days from today, at 9:00. Status is a plain string too, for the same reason as the role.
There is no CreatedBy or AssignedTo. Both need real users, so the seeder sets them.
Link the factory to the Task model:
package models
import ( "github.com/goravel/framework/contracts/database/factory" "github.com/goravel/framework/database/orm" "github.com/goravel/framework/support/carbon"
"taskflow/database/factories")
7 collapsed lines
type Status string
const ( StatusPending Status = "pending" StatusInProgress Status = "in-progress" StatusDone Status = "done")
type Task struct { orm.Model CreatedBy uint `gorm:"not null" json:"created_by"` Creator *User `gorm:"foreignKey:CreatedBy" json:"creator"` AssignedTo *uint `json:"assigned_to"` Assignee *User `gorm:"foreignKey:AssignedTo" json:"assignee"` Title string `gorm:"size:255;not null" json:"title"` Description string `gorm:"type:text;not null" json:"description"` Status Status `gorm:"type:enum('pending','in-progress','done');default:'pending';not null" json:"status"` DueAt *carbon.DateTime `json:"due_at"`}
func (t *Task) Factory() factory.Factory { return &factories.TaskFactory{}}Code The code for every step of this part is in the tag
part-07.
The user and task seeders
Make two seeders, one for users and one for tasks. Keeping them apart lets you run one alone, for example to add more tasks without touching the users:
go run . artisan make:seeder UserSeeder SUCCESS Seeder created successfully SUCCESS Seeder registered successfullygo run . artisan make:seeder TaskSeeder SUCCESS Seeder created successfully SUCCESS Seeder registered successfullyLike make:migration, make:seeder registers what it creates. It lists the seeders in a new bootstrap/seeders.go:
package bootstrap
import ( "github.com/goravel/framework/contracts/database/seeder"
"taskflow/database/seeders")
func Seeders() []seeder.Seeder { return []seeder.Seeder{ &seeders.UserSeeder{}, &seeders.TaskSeeder{}, }}db:seed runs them in this order. You made UserSeeder first, so the users exist before the tasks. make:seeder also added WithSeeders(Seeders) to bootstrap/app.go:
package bootstrap
11 collapsed lines
import ( "github.com/goravel/framework/contracts/database/schema" contractsfoundation "github.com/goravel/framework/contracts/foundation" "github.com/goravel/framework/foundation"
"taskflow/app/facades" "taskflow/app/models" "taskflow/config" "taskflow/routes")
func Boot() contractsfoundation.Application { return foundation.Setup(). WithSeeders(Seeders). WithMigrations(Migrations).11 collapsed lines
WithRouting(func() { routes.Api() routes.Web() }). WithProviders(Providers). WithConfig(config.Boot). WithCallback(func() { facades.Schema().Extend(schema.Extension{ Models: []any{models.User{}, models.Task{}}, }) }). Create()}Fill in Run in database/seeders/user_seeder.go:
package seeders
import ( "taskflow/app/facades" "taskflow/app/models")
type UserSeeder struct {}
// Signature The name and signature of the seeder.func (s *UserSeeder) Signature() string { return "UserSeeder"}
// Run executes the seeder logic.func (s *UserSeeder) Run() error { var manager models.User err := facades.Orm().Factory().Create(&manager, map[string]any{ "Name": "Ana", "Email": "ana@example.com", "Role": models.RoleManager, }) if err != nil { return err }
var members []models.User return facades.Orm().Factory().Count(3).Create(&members)}The first Create makes Ana, the manager. The map overrides what the factory would pick, so Ana always has the email ana@example.com. Her password is password, like every fake user’s. Count(3) then makes three members with fake names.
TaskSeeder can run alone, so it reads the users back from the database instead of getting them from UserSeeder. Fill in database/seeders/task_seeder.go:
package seeders
import ( "taskflow/app/facades" "taskflow/app/models")
type TaskSeeder struct {}
// Signature The name and signature of the seeder.func (s *TaskSeeder) Signature() string { return "TaskSeeder"}
// Run executes the seeder logic.func (s *TaskSeeder) Run() error { var manager models.User err := facades.Orm().Query().Where("role", models.RoleManager).FirstOrFail(&manager) if err != nil { return err }
var members []models.User err = facades.Orm().Query().Where("role", models.RoleMember).Find(&members) if err != nil { return err }
for _, member := range members { var tasks []models.Task err = facades.Orm().Factory().Count(2).Create(&tasks, map[string]any{ "CreatedBy": manager.ID, "AssignedTo": &member.ID, }) if err != nil { return err } }
var unassigned models.Task return facades.Orm().Factory().Create(&unassigned, map[string]any{ "CreatedBy": manager.ID, })}- If there is no manager yet,
FirstOrFailstops the seeder withrecord not found.Firstwould hand back an empty user instead. AssignedTotakes&member.IDbecause the field is a pointer.- One more task is made by Ana and assigned to nobody.
Running seeders with db:seed
db:seed runs every registered seeder:
go run . artisan db:seed ERROR fail to run seeder: ERROR: duplicate key value violates unique constraint "users_email_unique" (SQLSTATE 23505)It fails because Ana is still in the table from the earlier parts, and emails must be unique. A seeder only adds rows. It does not clear the tables first. If you started from the code at the top of this part, your tables were empty and it worked.
Resetting the database with migrate:fresh
migrate:fresh drops every table and runs all the migrations again, and --seed then runs the seeders. Run it either way, so your ids match the ones below:
go run . artisan migrate:fresh --seed SUCCESS Dropped all tables successfully INFO Running migration INFO Running: 20210101000002_create_jobs_table INFO Running: 20261022221906_create_users_table INFO Running: 20261023063944_create_tasks_table SUCCESS Migration success SUCCESS Database seeding completed successfully. SUCCESS Migration fresh successThe tables now hold the seeded rows:
docker compose exec postgres psql -U goravel -d taskflow \ -c "SELECT id, name, email, role, left(password, 7) AS hash FROM users" \ -c "SELECT id, assigned_to, title, status, due_at::date AS due FROM tasks" id | name | email | role | hash----+----------------+----------------------------+---------+--------- 1 | Ana | ana@example.com | manager | $2a$12$ 2 | Darion Whitney | francis.hansen@jensen.name | member | $2a$12$ 3 | Joany Silva | cpowell@richards.org | member | $2a$12$ 4 | Cleora Grady | ruby.murphy3@harris.io | member | $2a$12$(4 rows)
id | assigned_to | title | status | due----+-------------+-------------------------+-------------+------------ 1 | 2 | Add tests for tasks | in-progress | 2026-11-06 2 | 2 | Write the API docs | done | 2026-11-05 3 | 3 | Update the dependencies | done | 2026-10-25 4 | 3 | Fix the login bug | pending | 2026-11-15 5 | 4 | Update the dependencies | in-progress | 2026-10-28 6 | 4 | Write the API docs | in-progress | 2026-11-02 7 | | Reply to support emails | in-progress | 2026-11-15(7 rows)Your names, emails and dates are different, because they are random. The passwords start with $2a$12$, the start of a bcrypt hash, so they are real hashes now, not x.
Running one seeder
--seeder runs only the seeders you name, by the name their Signature returns. To get more tasks for the same users, run TaskSeeder alone:
go run . artisan db:seed --seeder=TaskSeeder SUCCESS Database seeding completed successfully.docker compose exec postgres psql -U goravel -d taskflow -c "SELECT count(*) FROM tasks" count------- 14(1 row)The same users got seven more tasks. Running UserSeeder alone would fail on Ana’s email, just like before. Run migrate:fresh --seed again whenever you want the starting data back.
The code for this part is in the tag part-07.