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
Top comments (0)