Part 7 of the Goravel series. The original on DevChalk has the colour-matched code, the figures and the full diffs. Code for this part: krishankumar01/goravel-taskflow/tree/part-07.
So far every row came from psql, typed by hand, and had to be typed again each time the tables were rebuilt. A factory describes one fake row, and a seeder uses factories to fill the database. With both, one command rebuilds the database and fills it with users and tasks.
Start from the Relationships: Has Many code:
git clone https://github.com/krishankumar01/goravel-taskflow cd goravel-taskflow git checkout part-06 cp .env.example .env go run . artisan key:generate docker compose up -d go run . artisan migrate The user factory
Users come first, because every task needs one. Create a factory for them:
$ go run . artisan make:factory UserFactory SUCCESS Factory created successfully It creates database/factories/user_factory.go with an empty Definition. Fill it in, using gofakeit for the fake values:
database/factories/user_factory.go
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. bcrypt is slow on purpose, which is fine for a few rows. Feature Testing lowers its cost for the tests, where the hashing adds up. - 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. When the task endpoints need these values too, we move them into a package of their own.
Then run:
$ go mod tidy The model finds its factory through a Factory method:
app/models/user.go
package models import ( + "github.com/goravel/framework/contracts/database/factory" "github.com/goravel/framework/database/orm" + + "taskflow/database/factories" ) // ... 7 lines not shown 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
Tasks get a factory the same way:
$ go run . artisan make:factory TaskFactory SUCCESS Factory created successfully Fill in its Definition:
database/factories/task_factory.go
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.
-
Statusis a plain string too, for the same reason as the role. - There is no
CreatedByorAssignedTo. Both need real users, so the seeder sets them.
Link the factory to the Task model:
app/models/task.go
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 lines not shown 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{} +} 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 successfully $ go run . artisan make:seeder TaskSeeder SUCCESS Seeder created successfully SUCCESS Seeder registered successfully Like make:migration, make:seeder registers what it creates. It lists the seeders in a new bootstrap/seeders.go:
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:
bootstrap/app.go
package bootstrap // ... 11 lines not shown func Boot() contractsfoundation.Application { return foundation.Setup(). + WithSeeders(Seeders). WithMigrations(Migrations). // ... 11 lines not shown Create() } Fill in Run in database/seeders/user_seeder.go:
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
Createmakes 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:
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 success The 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.
One command now rebuilds the database and fills it with users and tasks. The API still takes whatever data a client sends.
Previous: 06. Relationships: Has Many
Next: 08. Validation with Form Requests
Series: Goravel
